手把手,嘴对嘴,讲解UCOSII嵌入式操作系统的任务调度策略(三)

只愿长相守 提交于 2019-12-12 22:56:56

继续......

if (ticks > 0u) {                            /* 0 means no delay!                                  */
        OS_ENTER_CRITICAL();
        y            =  OSTCBCur->OSTCBY;        /* Delay current task                                 */
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;              /* Load ticks in TCB                                  */
        OS_EXIT_CRITICAL();
        OS_Sched();                              /* Find next task to run!                             */
    }

依然是这一部分,接下来的重点是这个函数:OS_Sched()

这个函数实在是太重要了,因此我不得不慎重。

首先看一下官方的注释:

/*********************************************************************************************************
* SCHEDULER
*
* Description: This function is called by other uC/OS-II services to determine whether a new, high
* priority task has been made ready to run. This function is invoked by TASK level code
* and is not used to reschedule tasks from ISRs (see OSIntExit() for ISR rescheduling).
*
*********************************************************************************************************/

从上面的说明可以看出,这个函数的作用,主要是用来调度当前已经进入了就绪状态的最高优先级任务,然后切换进去。

函数定义如下:

void  OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3u                           /* Allocate storage for CPU status register     */
    OS_CPU_SR  cpu_sr = 0u;
#endif

    OS_ENTER_CRITICAL();
    if (OSIntNesting == 0u) {                          /* Schedule only if all ISRs done and ...       */
        if (OSLockNesting == 0u) {                     /* ... scheduler is not locked                  */
            OS_SchedNew();
            OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
            if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy     */
#if OS_TASK_PROFILE_EN > 0u
                OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task      */
#endif
                OSCtxSwCtr++;                          /* Increment context switch counter             */
                OS_TASK_SW();                          /* Perform a context switch                     */
            }
        }
    }
    OS_EXIT_CRITICAL();
}

关于任务调度的部分肯定是原子操作,不允许任何中断存在,因此必须要关闭中断。

其次,任务调度不能发生在中断中以及任务调度器上锁的情况,因此必须加以判定。

上面的内容比较简单,去掉那些判断和宏开关,我们需要关注的重点主要在以下的部分:

OS_SchedNew();
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
if (OSPrioHighRdy != OSPrioCur) {         
 /* No Ctx Sw if current task is highest rdy     */
  OS_TASK_SW();                          /* Perform a context switch                     */
}

首先看这个函数:OS_SchedNew()

函数定义如下:

static  void  OS_SchedNew (void)
{
#if OS_LOWEST_PRIO <= 63u                        /* See if we support up to 64 tasks                   */
    INT8U   y;


    y             = OSUnMapTbl[OSRdyGrp];
    OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
#else                                            /* We support up to 256 tasks                         */
    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);
    }
#endif
}

因为我们的系统最高支持64个任务,所以去掉那些我们不需要关注的地方,函数定义简化如下:

static  void  OS_SchedNew (void)
{
    INT8U   y;

    y  = OSUnMapTbl[OSRdyGrp];
    OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);
}

看着好像很简单,整个函数就两句话,但是,只要能把这两句话给弄明白了,关于调度的东西基本上就都没问题了。

关于变量OSRdyGrp与数组OSRdyTbl[]的意义,相信各位都已经十分理解,分别代表组就绪状态和任务就绪状态,那么新出来的这个数组OSUnMapTbl[]又代表什么呢?它和任务就绪表有什么关系?

跟踪OSUnMapTbl数组的定义可以发现,这是一个常数数组,它里面的内容是只读的,定义如下:

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                   */
};

这个表还不小,一眼看去脑袋都大了,它到底是个什么玩意儿?别慌,听我慢慢讲解……

依然举个例子,现在我们的系统只有两个任务(0和12),当前的任务优先级是0,然后这个任务进入了延时,这个时候根据前面了解的东西:

 y            =  OSTCBCur->OSTCBY;        /* Delay current task */                             
 OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;

这个优先级为0的任务已经被设置为了未就绪状态,也就是把它的就绪表清空了,对应的OSRdyTbl[0]肯定是0,由于只有两个任务,因此对应的OSRdyGrp的最后一个bit位,也肯定是0,。

然后我们还有一个优先级为12的任务已经准备就绪, 那么代码执行到了这里,任务的OSRdyTbl[1]必然等于0x10,组号OSRdyGrp必然等于0x02,把2带进这个常数表,得到的结果是:1

这个“1”有什么意义?

y             = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);

结论:这两句代码真正的功能,就是从系统中,把当前已经就绪了的任务里,优先级最高的那个任务给找出来,而这个任务也就是我接下来要切换进去的那一个。

假设现在我们系统中只有两个任务,一个优先级为0(未就绪),一个优先级为12(就绪),现在我们就来看看,他到底是怎么把12这个数据给找出来的。

上面说了,当一个任务的优先级是确定数的时候,他的组号、组内坐席号,偏移量等都是确定的。

当只有任务12就绪时,这个时候组号OSRdyGrp必然等于0x02,那么把它带入那个常数表中,得到结果y为1。

在把1带入数组OSRdyTbl[1]中,等到结果的结果是0x10(参考上一节),把0x10带入常数表,得到的结果OSUnMapTbl[OSRdyTbl[y]] == 4

这个y等于1,把它向左移动3个bit,得到的结果是8(二进制00001000)……最后的结果8 + 4 = 12。

没想到它真的把我需要的优先级给算出来了,到底是怎么做到的?

其实,UCOSII使用的这种方法被叫查表法,根据一定的规律,直接计算出当前优先级最高的那个任务号。

那个看似莫名其妙的常数表OSUnMapTbl,其实它所代表的意思,是0~255个数字中,1所在的最低位:

比如1,二进制00000001,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[1] = 0,也就是第0位出现了1。

比如2,二进制00000010,在它的次低位出现了1,那么带入常数表一查,发现OSUnMapTbl[2] = 1,也就是第1位出现了1。

比如3,二进制00000011,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[3] = 0,也就是第0位出现了1。

  ……

比如12,二进制00001100,在它的第2位出现了1,那么带入常数表一查,发现OSUnMapTbl[12] = 2,也就是第2位出现了1。

比如63,二进制011111111,在它的最低位出现了1,那么带入常数表一查,发现OSUnMapTbl[3] = 0,也就是第0位出现了1。

因为有了这个表,算法上才可能做到无论有多少个任务进入了就绪的状态,我都能轻轻松松地取出优先级最高的那一个,根据变量OSRdyGrp的状态,我可以找到那些组有就绪的任务,

如果第0组和第3组内都有就绪的任务,那么OSRdyGrp肯定等于0x05,但是我根本不用关心第3组的状态,忽略即可,因为第0组明显优先程度更大,我只需要继续前往第0组内寻找便可。

进入第0组内部后,假如优先级为1和优先级为5的任务都就绪了,变量OSRdyTbl[0] == 0x22(二进制00100010),我肯定要执行优先级为1的任务,那么我就直接把这个数据带进那个常数表中去查寻,看看最低位出现1的位置,也就是就绪的任务到底是哪一个,

只要找到了它,别的任务就算是就绪状态,我也不用管了……查表法便是基于这个原理。

y             = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3u) + OSUnMapTbl[OSRdyTbl[y]]);

再看一下这两句代码,第一句代码的意思是:找到就绪任务中优先级最高的组号,比如1组和3组都就绪了,我需要的结果是:1。

第二句代码中的这句话OSUnMapTbl[OSRdyTbl[y]]的意思是:找到在这个组中,优先级最高的任务的坐席号,也就是偏移量,比如任务12和任务13都就绪了,我需要的结果是:4(代表任务12的偏移)。

整个第二个代码的意思是:把组号和组内坐席号组合起来,形成最后的任务优先级。

如果想不明白的话,可以参考上一章:

ptcb->OSTCBY             = (INT8U)(prio >> 3u);
ptcb->OSTCBX             = (INT8U)(prio & 0x07u);

这两句话的意思,在建立任务的时候,把一个好好的优先级给拆开,现在终于是把它们给重新合上了。

问:现在回过头看,OS_SchedNew这个函数的作用是什么?

答:很明显,它的作用就是寻找到,在当前已经就绪的任务中,优先级最高的那一个任务。

问:题外话,思考一个问题,为什么要用查表法呢?如果是自己来做这个策略,有没有其他的方法?

答:当然有,如果是我来做,或许可以建立一个大表,里面装有所有任务的就绪状态,然后写一个for循环,每次进行任务切换的时候,从低到高依次判断,如果任务状态位bit是1,那么就证明这个任务是就绪了的,立即跳出去进行任务切换,如果任务状态bit是0,那就证明这个任务没有就绪,继续进行下一个判断,我想这样肯定更容易理解一些。

不过这样做有一个问题,如果当前我就绪的任务优先级是0,那么在第一个循环就能找到任务,然后任务切换,时间比查表法块很多,如果我就绪的任务是255呢?那么我可能就需要循环255次才能找到就绪的任务,那么时间肯定会很长。

用这种方法会导致寻找就绪任务需要的时间完全不能确定,有时候短,有时候长,然而这对于一个系统而言,最怕的就是这种不确定因素。

查表法和循环法就完全不同了,它虽然死板一些,但不管当前系统有多少任务,不管当前有多少任务是处于就绪状态,它每次计算出最高优先级的任务的时间是一定的,这种确定性对于系统很重要。

待续......

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!