μC/OS-II 的任务调度算法分析

时间:2021-09-09 20:10:16
任务调度是一个操作系统最核心的部分,μC/OS-II 通过三步走来完成这个过程。
1) 从就绪表中获得就绪任务的最高优先级:OSRdyTbl[],OSRdyGrp,OSMapTbl[],OSUnMapTbl[]
2) 获取最高优先级就绪任务的任务控制块:OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]
3) 启动任务切换:OS_TASK_SW()

第一步:在最经典的μC/OS-II版本(2.52)中,仅仅支持64个任务,并且其ID即为它的任务优先级prio。所以要首先找到最高任务优先级OSPrioHighRdy。与此相关的全局变量有四个:OSRdyTbl[],OSRdyGrp,OSMapTbl[],OSUnMapTbl[],均为INT8U类型,分别介绍如下:
其中OSRdyTbl[]最大为8*8=64的bit的位置,分别表示64个任务的“坑”——就续表,这里简要介绍下对就续表的三大操作:设置清除寻优(找最高优先级);而OSRdyGrp为辅助就续表用来快速查找最高优先级用(后面介绍),故此变量在设置清除任务就续表时是不需要的,但是为了换取之后的快速找到最高任务优先级,这里牺牲一个1byte字节来空间来换取时间。具体解释参看《嵌入式实时系统μC/OS-II 》
 C++ Code 
1
2
INT8U   OSRdyGrp;                        /* Ready list group */
INT8U   OSRdyTbl[OS_RDY_TBL_SIZE];       /* Table of tasks which are ready to run */
μC/OS-II 的任务调度算法分析
PS1:引发就绪任务表的变化情况通常有以下几种情况:
● 当有新任务被创建,并且同时在就绪任务表中进行成功的登记
● 当有任务被执行删除操作时
● 当有处在等待状态的任务被某一事件或某一资源唤醒时
● 当有异步事件发生,有由中断服务程序激活的任务时
● 任务正在占用核心处理器,但因等待某一事件或某一资源进入等待状态
● 任务正在占用核心处理器,但因调用延时函数进入等待状态。
通过上面的分析,我们可以得知,μC/OS-II 中,调度器执行发生在所有的系统调用的末尾及中断程序结束之前这个阶段。
PS2:这里大家可能想到使用以下三种结构来表示就续表:数组链表最大堆。但是前两者在设置和清除上面的时间复杂度为O(1),但是寻优为O(n);虽然最大堆在寻优的时间复杂度为O(1),但是在设置和清除上面的时间复杂度为O(log n)。并且在实现 RTOS的时候,还不能使用动态内存分配,一方面是时间不确定,另一方面是要依赖别的实现库。我猜这也是μC/OS-II为什么会用一维数组的原因之一吧。
而仅仅靠这两个全局变量在寻优上面的时间复杂度还是O(n)的,所以需要另外的空间:OSMapTbl[],OSUnMapTbl[]来换取寻优的时间,使其寻优的时间复杂度为O(1),即与任务的多少没有关系。

 优先级位掩码查找表
1
2
3
4
5
6
7
8
9
10
/*
*********************************************************************************************************
*                              MAPPING TABLE TO MAP BIT POSITION TO BIT MASK
*
* Note: Index into table is desired bit position, 0..7
*       Indexed value corresponds to bit mask
*********************************************************************************************************
*/


INT8U  const  OSMapTbl[8]   = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};

Index       

Bit Mask (Binary)

0

00000001

1

00000010

2

00000100

3

00001000

4

00010000

5

00100000

6

01000000

7

10000000


 优先级索引查找表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
*********************************************************************************************************
*                                       PRIORITY RESOLUTION TABLE
*
* Note: Index into table is bit pattern to resolve highest priority
*       Indexed value corresponds to highest priority bit position (i.e. 0..7)
*********************************************************************************************************
*/


INT8U  const  OSUnMapTbl[256] = {
    0010201030102010,       /* 0x00 to 0x0F                             */
    4010201030102010,       /* 0x10 to 0x1F                             */
    5010201030102010,       /* 0x20 to 0x2F                             */
    4010201030102010,       /* 0x30 to 0x3F                             */
    6010201030102010,       /* 0x40 to 0x4F                             */
    4010201030102010,       /* 0x50 to 0x5F                             */
    5010201030102010,       /* 0x60 to 0x6F                             */
    4010201030102010,       /* 0x70 to 0x7F                             */
    7010201030102010,       /* 0x80 to 0x8F                             */
    4010201030102010,       /* 0x90 to 0x9F                             */
    5010201030102010,       /* 0xA0 to 0xAF                             */
    4010201030102010,       /* 0xB0 to 0xBF                             */
    6010201030102010,       /* 0xC0 to 0xCF                             */
    4010201030102010,       /* 0xD0 to 0xDF                             */
    5010201030102010,       /* 0xE0 to 0xEF                             */
    4010201030102010        /* 0xF0 to 0xFF                             */
};
PS:OSUnMapTbl[]这个数组的生成原则:先把一个数用二进制表示,然后从低位往高位数,返回第一次碰到1的位置。比如:OSUnMapTbl[0x111100(60)] = 2。可以看到,如果要表示8位数的对应关系,则数组的大小为2^8=256,16位的话就是2^16 = 65536。这也是为什么OSRdyTbl[],OSRdyGrp采用8位的原因。(当然可以通过一定的技巧来使其变为16位的,如掩位的技巧,后面通过扩展μC/OS-II支持的最大任务来说明这个技巧)
再从代码生成的角度看看是如何得到这个表的?
  OSUnMapTbl[]数组生成程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main(void)
{
    int i,t,n;
    int tab[256]={0};
    for(i=0;i<8;i++)
        for(t=1;(t<<i)<256;t++)
            tab[t<<i]=i;

    //output this tab
    for(n=0;n<=0xff;n++)
    {
        if(n%0x10==0)
            printf("\n");
        printf("%3d" , tab[n]);
    }
    printf("\n");   
}

好了,四个变量已经准备就绪了,来看看三大任务操作,程序清单如L1,L2L3所示。
 程序清单 L1. 将任务进入就绪态(置位操作)
1
2
OSRdyGrp |= OSMapTbl[prio >> 3];
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];
        先看第一行代码,把相应OSRdyGrp的值置为1。不妨假设prio的值为13-0x0000 1101, 即优先级为13, prio>>3 右移3位后值为0x0000 0001, 可以查OSMapTbl表找出 OSMapTbl[1] 的值为0x0000 0010. 再用0x0000 0010 和 OSRdyGrp 进行或运算,其结果:OSRdyGrp的bit2位必为1。
        再看第二行代码,任务优先级13的低三位(0x0000 0101)用于确定是在OSRdyTbl[prio >> 3]数组的第几位。 “prio & 0x07”用于限制OSRdyTbl[]数组的元素下标在0到7之间(或者我们可以这样理解:任务的优先级在[0~63范围,共分为8组,每组8个任务,用6位二进制可以表示,其中任务优先级的高三位理解为优先级所在的组(prio >> 3),低三位理解为所在组中的偏移量(prio & 0x07))。本例中的结果:OSRdyTbl[5]的bit5必为1。
μC/OS-II 的任务调度算法分析

 程序清单 L2. 将任务从就绪态中删除(清零操作)
1
2
if((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
    OSRdyGrp &= ~OSMapTbl[prio >> 3];
        第一行先对OSRdyTbl[]中某数据的某一位清0,然后进行判断,如果OSRdyTbl[]中这个数据全为0(也主相当于这个数据的所有8位都已经清0了),再对OSRdyGrp的某位清0。简单就是说,现在就绪位置0,然后看OSRdyTbl[]组中是否都为0,如果是0就将OSRdyGrp也清0。在这里应当知道,只有OSRdyTbl[]任务组中的所有就绪位都为0时,OSRdyGrp相应的为才为0。因为OSRdyGrp中的每一bit记录着OSRdyTbl[]中每一byte的信息,故当后者发生变化时需要去前者看看记录的信息是否需要相应的变化

  OSUnMapTbl[]数组主要是用于找出进入就绪态的优先级最高的任务。而这个地方也是我一开始没搞明白的,不明白OSUnMapTbl[]中的数值是怎么来的。
        先脱离所有上下文关系来说说OSUnMapTbl[]的一般意义。这里用二进制比较方便说明问题。OSUnMapTbl[]共0xFF个元 素,0x00~0xFF为索引,而OSUnMapTbl[]里的值就是通过分析索引得到的。比如说,索引0x50, 二进制表示为0101 0000,然后从右边数,看第几位首先为1,则OSUnMapTbl[0x50]的值就为几。易知,0101 0000从右起,第4位首先为1,所以有OSUnMapTbl[0x50]=4。再比如0x51,二进制为0101 0001,右起第0位为1,所以OSUnMapTbl[0x51]=0。
那为什么要从右数起呢?这个和优先级表有关系,优先级的值越小,优先级就越高。再看上面那幅优先级的结构图,可见,优先级是从右至左,从上至下越来越低的,最低优先级给了空闲任务。
        OSRdyTbl[]中每个字节的8位代表这一组的8个任务哪些进入就绪态,且低位的优先级高于高位。首先,利用OSRdyGrp查找OSUnMapTbl表可以得到就绪态中优先级最高任务所在的组;然后,再利用OSRdyTbl[OSUnMapTbl[OSRdyGrp]] 查找OSUnMapTbl表可以得到就绪态中优先级最高任务所在组中的偏移值,计算后就可以获得就绪态中最高优先级是多少。刚才的冗余空间OSRdyGrp在这里发挥作用了,这里可以简单的看成两级的关系,第一级为OSRdyGrp,第二级为OSRdyTbl[],此处先找到8组(OSRdyGrp)里面最小的组,然后再在最小组(OSRdyTbl[OSUnMapTbl[OSRdyGrp]])中寻找最小单元。代码如下: 
 程序清单 L3. 找出所有就绪态任务中的优先级最高的任务 
1
2
3
4
y    = OSUnMapTbl[OSRdyGrp];
x    = OSUnMapTbl[OSRdyTbl[y]];
prio = (y << 3) + x;           /* 括号一定要加:优先级 */ 

            x、y的含义看上面的图就知道了:y是“高三位”,x是“低三位”。
        找最高优先级任务的过程是这样的(解释OSUnMapTbl的作用,原理):首先,查OSRdyGrp,看OSRdyGrp中右起的第几位首先为1,比如OSRdyGrp=0x56,0x56的二 进制为0101 0110,可见右起第1位首先为1,所以y=OSUnMapTbl[0x56]=1,然后再去OSRdyTbl[y]即OSRdyTbl[1]中查找(为什么是OSRdyTbl[y],这个书上说得很明确,这个得清楚得了解OSRdyGrp和OSRdyTbl[]之间的关系),这里假设 OSRdyTbl[1]=0xD4,即1101 0100,同样找到OSRdyTbl[1]中右起的第2位首先为1,这样得到x=2,再通过第3行的移位运算就可以得到最高优先级的任务的优先级了。
        OSUnMapTbl[]就绪表其实就是找出在OSRdyGrp中那个就绪的优先级最高,因为不止一个位就绪,所以OSRdyGrp的取值范围可以是0x00~0xff,这样每一值都有对应的优先级,同理在OSRdyTbl[]对应的组中,通过就续表可以找出在这组就绪任务中那个优先级最高。
PS:⊙﹏⊙b汗,讲的有点小啰嗦。

第二步:第二步和第三步其实都是任务调度器(OS_Sched)的工作,分开讲感觉逻辑上好理解一点。
找到了最高优先级(OSPrioHighRdy)之后,就可以通过最高最优先级定位到最高优先级TCB,靠的就是OSTCBPrioTbl[]这个全局变量,里面放着指向相对应优先级的TCB指针。
 通过最高优先级获取最高优先级任务控制块(TCB)
1
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
μC/OS-II 的任务调度算法分析
任务的TCB才是真正代表着一个任务的实体。因此,找到了TCB就意味着完全可以掌握这个任务了。为什么那么说呢,看下面那张图就一目了然了。
μC/OS-II 的任务调度算法分析
任务的三大组成部分:任务控制块(TCB),任务堆栈,任务程序代码。而我们又可以通过TCB找到其它两个。

第三步:既然找到了最高优先级的任务(假设它目前处于就续态),那么就需要切换任务了。执行OS_TASK_SW()宏:
 os_cpu.h
1
#define  OS_TASK_SW()         OSCtxSw()
而OSCtxSw()通过触发一个中断来完成真正的任务切换,不过后面的工作都是μC/OS-II 使用者在移植时需要的做的工作。参考Google搜索。
简要说明,任务的切换严格按照以下的步骤进行,它们不能进行执行顺序的颠倒。如果顺序颠倒,会引发系统的混乱。完成的工作如下:
① 把当前正在执行的任务的断点指针保护到这个任务对应的任务堆栈中。
② 把核心处理器中寄存器的内容保存到这个任务的任务堆栈中。
③ 获取当前的堆栈指针,并且该任务的任务控制块的地址,把前者的值保存到后者中。
④ 获得就绪任务的任务控制块,且是通过查表得出的最高优先级的任务。
⑤ 将最高优先级就绪任务的任务堆栈指针调入到核心处理器中。
⑥ 将最高优先级就绪任务的任务堆栈中各通用寄存器的内容保存到核心处理器的各通用寄存器中。
⑦ 将最高优先级就绪任务的断点指针调入核心处理器。

 os_core.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//任务调度
//描述: uC/OS-II总是运行进入就绪态任务中优先级最高的那一个。确定哪个任务优先级最高,下面该哪
//      个任务运行了的工作是由调度器(Scheduler)完成的。任务级的调度是由函数OSSched()完成的。
//      中断级的调度是由另一个函数OSIntExt()完成的eduling).
//参数: none
//返回: none
//注意: 1) 这是一个uC/OS-II内部函数,你不能在应用程序中使用它
//      2) 给调度器上锁用于禁止任务调度 (查看 OSSchedLock()函数)
//说明: 1)任务切换很简单,由以下两步完成,将被挂起任务的微处理器寄存器推入堆栈,然后将较高优先
//      级的任务的寄存器值从栈中恢复到寄存器中。在uC/OS-II中,就绪任务的栈结构总是看起来跟刚
//      刚发生过中断一样,所有微处理器的寄存器都保存在栈中。换句话说,uC/OS-II运行就绪态的任
//      务所要做的一切,只是恢复所有的CPU寄存器并运行中断返回指令。为了做任务切换,运行
//      OS_TASK_SW(),人为模仿了一次中断。多数微处理器有软中断指令或者陷阱指令TRAP来实现上述操
//      作。中断服务子程序或陷阱处理(Trap hardler),也称作事故处理(exception handler),必须提
//      供中断向量给汇编语言函数OSCtxSw()。OSCtxSw()除了需要OS_TCBHighRdy指向即将被挂起的任务,
//      还需要让当前任务控制块OSTCBCur指向即将被挂起的任务,参见第8章,移植uC/OS-II,有关于
//      OSCtxSw()的更详尽的解释。
//      2) OSSched()的所有代码都属临界段代码。在寻找进入就绪态的优先级最高的任务过程中,为防止中
//      断服务子程序把一个或几个任务的就绪位置位,中断是被关掉的。为缩短切换时间,OSSched()全
//      部代码都可以用汇编语言写。为增加可读性,可移植性和将汇编语言代码最少化,OSSched()是用
//      C写的。


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


    OS_ENTER_CRITICAL();
    //为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将
    if ((OSIntNesting == 0) && (OSLockNesting == 0)) { /* Sched. only if all ISRs done & not locked    */
    //如果函数不是在中断服务子程序中调用的,且调度允许的,则任务调度函数将找出进入就绪态的
    //最高优先级任务,进入就绪态的任务在就绪表中OSRdyTbl[ ]中相应位置位.
        y             = OSUnMapTbl[OSRdyGrp];          /* Get pointer to HPT ready to run              */
        OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
        //找到最高优先级任务后,函数检查这个优先级最高的任务是否是当前正在运行的任务,以避免不
        //必要的任务调度,多花时间
       if (OSPrioHighRdy != OSPrioCur) {              /* No Ctx Sw if current task is highest rdy     */
        //为实现任务切换,OSTCBHighRdy必须指向优先级最高的那个任务控制块OS_TCB,这是通过将
        //以OSPrioHighRdy为下标的OSTCBPrioTbl[]数组中的那个元素赋给OSTCBHighRdy来实现的
            OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
            OSCtxSwCtr++;                              //统计计数器OSCtxSwCtr加1,以跟踪任务切换次数
            OS_TASK_SW();                              //最后宏调用OS_TASK_SW()来完成实际上的任务切换
        }
    }
    OS_EXIT_CRITICAL();
}

参考资料:
《嵌入式实时操作系统μC/OS-II 》(第2版)