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初始化OSIntNestingOSTaskCtrOSRunning等一些变量。

  2、调用函数OS_InitRdyList初始化准备运行的OSRdyTbl表位都为空。

  3、调用函数OS_InitTCBListOSTCBPrioTbl表的数据都初始化为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来获取当前已经准备好的最高优先级的任务(最高优先级主要通过OSRdyGrpOSRdyTbl来计算得到),然后调用函数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差不多,只不过它会调用函数OSStartHighRdycpuPSP初始化为0来运行我们系统中的第一个任务并将OSRunning标记为true来表示我们的内核开始运行了。

5 任务切换时机

  从OS_Sched函数和OSStartHighRdy函数可以看出,它们总是选择当前可运行的最高优先级任务来运行,所以只有最高优先级的任务不让出cpu,低优先级的任务永远也不可能运行,所以发生任务切换的唯一时机就是高优先级的进行主动让出cpu。高优先级的任务可以调用函数OSTimeDlyHMSM来让自己延迟从而让出cpuOSTimeDlyHMSM本质上是调用函数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位的值。然后合并这两个值就可以获得最高优先级的值了。

posted @ 2013-12-03 16:23  在于思考  阅读(3124)  评论(1编辑  收藏  举报