UCOSII内核代码分析
1 UCOSII定义的关键数据结构
OS_EXT INT8U OSIntNesting;
OSIntNesting用于判断当前系统是否正处于中断处理例程中。
OS_EXT INT8U OSPrioCur;
OSPrioCur表示当前进程的优先级。
OS_EXT INT8U OSPrioHighRdy;
OSPrioHighRdy表示最高优先级任务的优先级。
OS_EXT OS_PRIO OSRdyGrp;
OSRdyGrp主要来标记可运行任务优先级除去低位3位或4位后的group bit位。例如任务的优先级为65(假设超过了系统最大的优先级超过了63,那么OS_PRIO应该有16位),由于超过了63,所以这个任务的group bit位为2^(65>>4)=2^4,也就是OSRdyGrp的第5位(从低位开始算起)为1。
OS_EXT OS_PRIO OSRdyTbl[OS_RDY_TBL_SIZE];
准备运行的任务标记,对应位为1表示任务可以运行,否则不能。
OS_EXT BOOLEAN OSRunning;
标志内核是否正在运行。
OS_EXT INT8U OSTaskCtr;
任务被创建的个数。
OS_EXT OS_TCB *OSTCBCur;
指向当前正在运行任务的TCB(任务控制块)。
OS_EXT OS_TCB *OSTCBFreeList;
空闲TCB块指针。
OS_EXT OS_TCB *OSTCBHighRdy;
指向最高优先级任务的TCB块。
OS_EXT OS_TCB *OSTCBList;
正在使用的TCB块的双向链表。
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u];
每个优先级对应的已创建的TCB块。
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];
系统静态分配的一个系统中所有的TCB块,包括正在使用的和空闲的。
typedef struct os_tcb { OS_STK *OSTCBStkPtr; //指向任务的栈顶 struct os_tcb *OSTCBNext; //指向TCB链表中的下一个TCB块 struct os_tcb *OSTCBPrev; //指向TCB链表中的前一个TCB块 INT32U OSTCBDly; //任务延迟时间 INT8U OSTCBStat; //任务当前状态 INT8U OSTCBStatPend; //任务悬挂状态 INT8U OSTCBPrio; //任务优先级(0表示最高优先级) INT8U OSTCBX; //与任务优先级一致group bit位(一般是OSTCBPrio的低3位) INT8U OSTCBY; //与任务优先级一致的ready table(一般是OSTCBPrio高5位) OS_PRIO OSTCBBitX; //访问ready table的bit mask OS_PRIO OSTCBBitY; //访问ready group的bit mask } OS_TCB
TCB块是任务的描述符,描述了任务的状态和运行时必须的信息。
2 系统初始化
在OSInit函数中初始化系统的各个数据结构,具体如下:
1、调用函数OS_InitMisc初始化OSIntNesting,OSTaskCtr,OSRunning等一些变量。
2、调用函数OS_InitRdyList初始化准备运行的OSRdyTbl表位都为空。
3、调用函数OS_InitTCBList和OSTCBPrioTbl表的数据都初始化为0,第i个控制块指向第i+1个控制块,并且都加入到OSTCBFreeList指向的链表中,把OSTCBList初始化为空链表。
4、调用函数OS_InitTaskIdle来创建一个Idle任务,该任务是优先级最低的任务,任务号为65535,优先级为63。
3 创建启动任务
然后我们自己调用OSTaskCreate函数创建我们自己的任务,具体如下:
1、先判断系统当前是否处于中断,如果是,则返回,因为当系统处于中断时是不允许创建任务的。
2、判断需要创建任务的优先级是否存在其它相同优先级的任务,如果存在,则返回,即相同优先级的任务只能有一个。
3、调用函数OSTaskStkInit初始化任务的栈。
4、调用函数OS_TCBInit初始化任务的TCB块。这个函数主要是从OSTCBFreeList链表获取空闲的TCB块并初始化这个控制块,然后将这个控制块从OSTCBFreeList链表中删除,并插入到OSTCBList链表中。
如果现在内核正在运行,那么调用函数OS_Sched切换任务。
OS_Sched函数主要用来切换任务,如果当前系统不处于中断例程中,就通过调用函数OS_SchedNew来获取当前已经准备好的最高优先级的任务(最高优先级主要通过OSRdyGrp和OSRdyTbl来计算得到),然后调用函数OS_TASK_SW来将任务切换到当前优先级最高的任务。
OS_TASK_SW函数则调用os_cpu_a.asm文件中的汇编函数OSCtxSw来触发PendSV异常来完成任务的切换,至于为什么要这样做可以参考前面讲的重点的PendSV。
PendSV异常的处理的伪代码如下:
PendSVHandler: if (PSP != NULL) {//判断程序的堆栈指针是否为空,为空说明这是第一个任务 //当调用PendSVHandler()时, //CPU 就会自动保存xPSR、PC、LR、R12、R0-R3 寄存器到堆栈 //保存后,CUP 的栈SP指针会切换到使用主堆栈指针MSP上 //我们只需检测进入栈指针PSP是否为NULL就知道是否进行任务切换 //因此当我们第一次启动任务是,OSStartHighRdy()就把PSP设为NULL, //避免系统以为已经进行任务切换 Save R4-R11 onto task stack; //手动保存 R4-R11 OSTCBCur->OSTCBStkPtr = SP; //保存进入栈指针PSP到任务控制块 //以便下次继续任务运行时继续使用原来的栈 } OSTaskSwHook(); //此处便于我们使用钩子函数来拓展功能 OSPrioCur = OSPrioHighRdy; //获取最高优先级就绪任务的优先级 OSTCBCur = OSTCBHighRdy; //获取最高优先级就绪任务的任务控制块指针 PSP = OSTCBHighRdy->OSTCBStkPtr; //保存进入栈指针 Restore R4-R11 from new task stack; //从新的栈恢复 R4-R11 寄存器 Return from exception; //返回
PendSV异常处理代码很简单,就是保存现场,将当前程序的堆栈指针保存到TCB块中,并将下一个要运行任务的栈指针更新到PSP中,同时恢复下一个运行任务的现场。
4 运行系统
一切都已经准备就绪,那么我们就调用函数OSStart运行系统。该函数和任务切换函数OS_TASK_SW差不多,只不过它会调用函数OSStartHighRdy将cpu的PSP初始化为0来运行我们系统中的第一个任务并将OSRunning标记为true来表示我们的内核开始运行了。
5 任务切换时机
从OS_Sched函数和OSStartHighRdy函数可以看出,它们总是选择当前可运行的最高优先级任务来运行,所以只有最高优先级的任务不让出cpu,低优先级的任务永远也不可能运行,所以发生任务切换的唯一时机就是高优先级的进行主动让出cpu。高优先级的任务可以调用函数OSTimeDlyHMSM来让自己延迟从而让出cpu,OSTimeDlyHMSM本质上是调用函数OSTimeDly来实现让出cpu的操作。下面来看看这个函数的具体实现:
void OSTimeDly (INT32U ticks) { if (OSIntNesting > 0u) { //系统正处于中断中 return; } if (OSLockNesting > 0u) { //调度器加锁了 return; } if (ticks > 0u) { //延迟时间大于0 OS_ENTER_CRITICAL(); y = OSTCBCur->OSTCBY; //获取当前优先级的高位 OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX; //清空表中对应当前任务的bit位 if (OSRdyTbl[y] == 0u) { OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY; } OSTCBCur->OSTCBDly = ticks; //设置当前任务的延迟时间 OS_EXIT_CRITICAL(); OS_Sched(); //切换任务 } }
这个函数其实就是将本任务在OSRdyTbl表和OSRdyGrp对应的标记位清0,使得后面的调度函数OS_Sched可以选择其它优先级的任务,即当前任务让出cpu。这个函数还做的事就是设置当前任务的延迟时间到任务的TCB块中,这个时间会在SysTick处理函数中每个节拍的减少,直到减为0时,会将这个任务对应的bit位加入到OSRdyTbl表和OSRdyGrp中来获取cpu。
6 任务调度算法
UCOSII涉及的调度算法比较的简单,正如在任务切换时机里说的,只要高优先级的任务不让出cpu,低优先级的任务永远不可能得到cpu,调度器总是选择系统中已经就绪的最高优先级任务运行。问题就来了,调度器怎样确定到当前系统中任务的最高优先级是多少呢?当然最笨的方法是将OSTCBList链表都遍历一遍,这样肯定是可以的。但这样效率是很低的,特别是系统中就绪任务比较多的时候,它的时间复杂度位O(n),n为系统中就绪任务的个数。UCOSII采用了一种比较巧妙的方法实现在O(1)的复杂度获得这个最高优先级值。
当一个任务就绪时,这个任务的优先级OSTCBPrio被拆分成两个部分,低4位的值存放在任务控制块的OSTCBX中,高4位存放在OSTCBY中。然后将2^OSTCBX的值存放在控制块的OSTCBBitX中,将2^OSTCBY存放在OSTCBBitY中。通过下面方法更新OSRdyTbl表和OSRdyGrp变量。
OSRdyGrp |= ptcb->OSTCBBitY;
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
OSRdyGrp存放了当前系统中所有就绪任务优先级高4位表示的bit位,OSRdyTbl表其实就是一个位图,用来标记系统中所有就绪任务的优先级,每一个优先级对应一个bit位。
有了这些信息,怎样来获取当前就绪任务中最高优先级的值呢?系统维护了一张表:
INT8U const OSUnMapTbl[256] = {
0u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x00 to 0x0F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x10 to 0x1F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x20 to 0x2F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x30 to 0x3F*/
6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x40 to 0x4F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x50 to 0x5F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x60 to 0x6F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x70 to 0x7F*/
7u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x80 to 0x8F*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0x90 to 0x9F*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xA0 to 0xAF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xB0 to 0xBF*/
6u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xC0 to 0xCF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xD0 to 0xDF*/
5u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, /* 0xE0 to 0xEF*/
4u, 0u, 1u, 0u, 2u, 0u, 1u, 0u, 3u, 0u, 1u, 0u, 2u, 0u, 1u, 0u /* 0xF0 to 0xFF*/
};
通过函数OS_SchedNew来获取最高的优先级值:
static void OS_SchedNew (void) { INT8U y; OS_PRIO *ptbl; if ((OSRdyGrp & 0xFFu) != 0u) { y = OSUnMapTbl[OSRdyGrp & 0xFFu]; } else { y = OSUnMapTbl[(OS_PRIO)(OSRdyGrp >> 8u) & 0xFFu] + 8u; } ptbl = &OSRdyTbl[y]; if ((*ptbl & 0xFFu) != 0u) { OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(*ptbl & 0xFFu)]); } else { OSPrioHighRdy = (INT8U)((y << 4u) + OSUnMapTbl[(OS_PRIO)(*ptbl >> 8u) & 0xFFu] + 8u); } }
这个函数先根据OSRdyGrp值这张表来获得最高优先级的高4位的值y,然后通过OSRdyTbl[y]来获得低4位的值x,这样最终的值就是y<<4 + x。为什么这样就可以得到最高优先级呢?现在来看一个例子:假设OSRdyGrp = 10,对应的二进制值为1010,那么就绪任务中优先级前4位最小的值为1,由于OSRdyGrp的二进制值第二位为1(这个可以通过前面计算OSRdyGrp的方法来理解)。然后通过这个1,我们可以知道最高优先级在OSRdyTbl对应的bit位在OSRdyTbl[1]中,然后通过OSRdyTbl[1]来查表OSUnMapTbl同样可以获得最高优先级的低4位的值。然后合并这两个值就可以获得最高优先级的值了。