μC/OS-II与RT-Thread对比——任务调度

       在任务调度器的实现上,μC/OS-II和RT-Thread都采用了位图调度(bitmap scheduling),任务优先级的值越小则代表具有越高的优先级,主要区别在于实现形式,是采用多级队列的形式,还是纯位图的形式。在位图调度下,每当需要进行调度时,从最低位向最高位查找出第一个置 1 的位的所在位置,即为当前最高优先级,然后从对应优先级就绪队列获得相应的任务控制块,整个调度器的实现复杂度是 O(1),即无论任务多少,其调度时间是固定的。采用多级队列的方式,一个优先级可以分配给多个任务,任务数不受限制,相同优先级的任务支持协作调度;采用纯位图的实现方式,一个优先级只能分配给一个任务,任务数有限,但确定性更好。

1. 任务控制块

        任务控制块是操作系统用于控制任务的一个数据结构,会存放任务相关信息,例如优先级、任务堆栈等等。在RT-Thread中叫做线程控制块。

μC/OS-II与RT-Thread对比——任务调度_第1张图片

(1)μC/OS-II

μC/OS-II与RT-Thread对比——任务调度_第2张图片

(2)RT-Thread



μC/OS-II与RT-Thread对比——任务调度_第3张图片

        宏观看上去,μC/OS-II的代码更规范化,每个定义都是OSTCB开头的,但是RT-Thread的更清晰明了,更具有gcc的编码风格。从内容上讲,两者都具有最基础的堆栈指针、优先级、对象链表、事件控制域、以及调度需要使用的优先级相关变量,但是RT-Thread还具有一些其他的特性:支持相同优先级的线程链表,线程清理函数,线程定时器等等。

2. 任务调度模型

(1)μC/OS-II

       因为μC/OS-II总是运行进入就绪状态的最高优先级的任务。所以,确定哪个任务优先级最高,下面该哪个任务运行,这个工作就是由调度器(scheduler)来完成的。任务级的调度是由函数OSSched()完成的,而中断级的调度是由函数OSIntExt()完成。对于OSSched(),它内部调用的是 OS_TASK_SW()完成实际的调度(人为模仿一次中断);OSIntExt()内部调用的是OSCtxSw()实现调度。
 
        μC/OS-II与RT-Thread对比——任务调度_第4张图片

(2)RT-Thread

        RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。系统总共支持256个优先级(0 ~ 255,数值越小的优先级越高,0为最高优先级,255分配给空闲线程使用,一般用户不使用。在一些资源比较紧张的系统中,可以根据实际情况选择只支持8个或32个优先级的系统配置)。当系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。如图 线程就绪优先级队列 所示,在RT-Thread调度器的实现中,包含了一个共256个优先级队列的数组(如果系统最大支持32个优先级,那么这里将是一个包含了32个优先级队列的数组),每个数组元素中放置相同优先级链表的表头。这些相同优先级的列表形成一个双向环形链表,最低优先级线程链表一般只包含一个idle线程。
μC/OS-II与RT-Thread对比——任务调度_第5张图片
       在优先级队列1#和2#中,可以看到三个线程:线程A、线程B和线程C。由于线程A、B的优先级比线程C的高,所以此时线程C得不到运行,必须要等待优先级队列1#的中所有线程(因为阻塞)都让出处理器后才能得到执行。同优先级的线程采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级
就绪线程存在的情况下才有效。例如在 线程就绪优先级队列 图中,我们假设线程A和线程B一次最大允许运行的时间片分别是10个时钟节拍和7个时钟节拍。那么线程B将在线程A的时间片结束(10个时钟节拍)后才能运行,但如果中途线程A被挂起了,即线程A在运行的途中,因为试图去持有不可用的资源,而导致线程状态从就绪状态更改为阻塞状态,那么线程B会因为其优先级成为系统中就绪线程中最高的而马上运行。每个线程的时间片大小都可以在初始化或创建这个线程时指定。
        RT-Thread实时操作系统提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回的变换。例如一个就绪态的线程由于申请一个资源(例如使用rt_sem_take),而可能进入挂起态。又例如因为一个外部中断发生了,系统转入中断服务例程,在中断服务例程中释放了相应的资源,导致把等待在这个资源上的高优先级线程唤醒,改变其状态为就绪态,导致当前运行线程切换等等。几种状态间的转换关系如 线程转换图 所示:
μC/OS-II与RT-Thread对比——任务调度_第6张图片
        线程通过调用函数rt_thread_create/init进入到初始状态(RT_THREAD_INIT);再通过调用函数rt_thread_startup进入到就绪状态(RT_THREAD_READY);当处于就绪状态的线程调用rt_thread_delay,rt_sem_take,rt_mb_recv等函数或由于获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用rt_thread_delete/detach将更改为关闭状态(RT_THREAD_CLOSE);而运行状态的线程,如果运行结束会在线程最后部分执行rt_thread_exit函数而更改为关闭状态(RT_THREAD_CLOSE)。

3. 任务调度算法

(1)μC/OS-II

       uCOS II采用内存映射的方式来实现READY队列的加入,查找,删除功能,效率非常高。但是也因此只能支持64个任务,每个任务都有自己的优先级,不能和其他任务优先级相同。
μC/OS-II与RT-Thread对比——任务调度_第7张图片
        每个任务的就绪态标志都放入就绪表中的,就绪表中有两个变量OSRdyGrp和OSRdyTbl[]。(OSRdyGrp占用一个字节:8位,每位代表一个优先级组;OSRdyTbl[]占8个字节,共64位,每位代表一个优先级)在OSRdyGrp中,任务按优先级分组,8个任务为一组。OSRdyGrp中的每一位表示8组任务中每一组中是否有进入就绪态的任务。任务进入就绪态时,就绪表OSRdyTbl[]中的相应元素的相应位也置位。就绪表OSRdyTbl[]数组的大小取决于OS_LOWEST_PRIO(见文件OS_CFG.H)。

     为确定下次该哪个优先级的任务运行了,内核调度器总是将OS_LOWEST_PRIO在就绪表中相应字节的相应位置1。OSRdyGrp和OSRdyTbl[]的关系见图3.3,是按以下规则给出的: 当OSRdyTbl[0]中的任何一位是1时,OSRdyGrp的第0位置1,
    当OSRdyTbl[1]中的任何一位是1时,OSRdyGrp的第1位置1,
    当OSRdyTbl[2]中的任何一位是1时,OSRdyGrp的第2位置1,
    以些类推。。。。。
    任务就绪的掩码表:
    INT8U const OSMapTbl[]   = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};//定位优先级所用

    任务优先级的低三位用于确定任务在总就绪表OSRdyTbl[]中的所在位。接下去的[5:3]三位用于确定是在OSRdyTbl[] 数组的第几个元素。 OSMapTbl[]是在ROM中的(见文件OS_CORE.C)屏蔽字,用于限制OSRdyTbl[]数组的元素下标在0到7之间:

        下面的代码用于将任务放入就绪表,使任务进入就绪态 (这两行代码是关键)。Prio是任务的优先级。

       这行代码功能是找到组, 把组上的值置为1。不妨假设prio的值为13, 即优先级为13. prio>>3 右移3位后值为1, 可以查表T3.1找出OSMapTbl[1] 的值为 0000 0010. 再用 0000 0010 和 OSRdyGrp 进行异或运算:
OSRdyGrp |= OSMapTbl[prio >> 3];   //先根据高3位[5:3]找到所在的组
OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07]; //再由低3位把该组的具体某一位置1

      如果一个任务被删除了,则用下边的代码做求反处理,从就绪表中删除一个任务

if ((OSRdyTbl[prio >> 3] &= ~OSMapTbl[prio & 0x07]) == 0)
    OSRdyGrp &= ~OSMapTbl[prio >> 3];

       以上代码将就绪任务表数组OSRdyTbl[]中相应元素的相应位清零,而对于OSRdyGrp,只有当被删除任务所在任务组中全组任务一个都没有进入就绪态时,才将相应位清零。也就是说OSRdyTbl[prio>>3]所有的位都是零时,OSRdyGrp的相应位才清零。为了找到那个进入就绪态的优先级最高的任务,并不需要从OSRdyTbl[0]开始扫描整个就绪任务表,只需要查另外一张表,即优先级判定表 OSUnMapTbl ([256])(见文件OS_CORE.C)。OSRdyTbl[]中每个字节的8位代表这一组的8个任务哪些进入就绪态了,低位的优先级高于高位。利用这个字节为下标来查OSUnMapTbl这张表,返回的字节就是该组任务中就绪态任务中优先级最高的那个任务所在的位置(即该字节的中最低位1的所在位)。这个返回值在0到7之间。确定进入就绪态的优先级最高的任务是用以下代码完成的。
       找出进入就绪态的优先级最高的任务

y    = OSUnMapTbl[OSRdyGrp];
x    = OSUnMapTbl[OSRdyTbl[y]];
prio = (y << 3) + x;
       优先级查询表:
INT8U const OSUnMapTbl[] = {
    0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
    4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};//定位最高优先级用(即查找一个字节的最低位1所在的位置)
     例如,如果OSRdyGrp的值为二进制01101000,查OSUnMapTbl[OSRdyGrp]得到的值是3,它相应于OSRdyGrp中的第3 位bit3,这里假设最右边的一位是第0位bit0。类似地,如果OSRdyTbl[3]的值是二进制11100100,则OSUnMapTbl [OSRdyTbc[3]]的值是2,即第2位。于是任务的优先级Prio就等于26(3*8+2)。利用这个优先级的值。查任务控制块优先级表 OSTCBPrioTbl[],得到指向相应任务的任务控制块OS_TCB的工作就完成了。

(2)RT-Thread

        RT-Thread的调度算法与μC/OS-II基本是一样的,都是位图调度,但是在这个基础上有一定的扩展,因为在RT-Thread系统存在多个线程时,可能的情况是,某些线程具有不同的线程优先级,但是还有一些线程具有相同的优先级。对于这种情况,rt-thread采用的调度策略是,对不同优先级的线程,采用可抢占的方式:即高优先级的线程会“立刻”抢占低优先级的线程,而对同线程优先级别的多个线程则采用时间片轮转的方式。
       首先来考虑,每一个优先级上是否存在线程,这是一个是/否问题,即要么存在线程,要么不存在线程,这可以用一个bit位来表示。我们规定这个bit为1表示存在线程,为0表示不存在线程。对于256级的线程,则共需要256个bit位。这个位图要比μC/OS-II复杂一些,如下:
       单元格中的内容表示对应的优先级。 每一行为对应的一个字节,每一列为各个bit位。

       这张图就是前面scheduler.c中定义的32个字节的数组 (rt_uint8_t rt_thread_ready_table[32])。
       举个例子,我们创建了一个线程,并且指定了它的优先级是125,然后将它设置为就绪(READY),实际上在我们在调用函数将它变为READY的函数中,RTT就会去上面这256个bit中(也即是这32个字节),找到第125个bit,我称之为位图的BIT125, 也即是字节15 (125/ 8 = 15,125%8 = 5)的第5个bit,将这个bit置1。 即位图的BIT125 就是rt_thread_ready_table[125/8]的BIT5.我们可以用位代码表示为 BITMPA.BIT_125 = rt_thread_ready_table[125/8].BIT5
       优先级125 对应那个字节的哪个bit呢?
       这里有个换算关系。其计算公式 :
       (优先级别 / 8 )商取整数即对应位图中的字节 (优先级别 % 8 )就是对应位图字节中的bit位,即优先级125, 125 / 8 = 15 , 125 %8 = 5. 位图的BIT125就是 rt_thread_ready_table[15]的BIT5
       现在我们将位图看作一个变量,并假定当前优先级别为8,则位图变量可以用一个字节表示。此时和μC/OS-II的算法是一样的,同样设置一个查找表,实际上,查表法就是一种常用的用空间换取时间的方法。

        进程优先级为8时,我们可以查表直接解决,当系统存在32个优先级时,如果直接制作表格的话,这个表格的元素个数将是 2**32 = 4294967296L= 4G字节。显然这是不可接受的。
        32个优先级,即优先级位图变量可以使用u32型,也就是等价于4个字节,我们可以对这4个字节从字节0开始依次查表,如果字节0中非0,则最高优先级一定存在于字节0中,我们对字节0查表rt_lowest_bitmap,即可以得到最高优先级。 如果字节0为0,字节1非0,我们对字节1查表得到的是字节1中为1的最低bit位,然后加上8,就是系统的最高优先级。对字节2,字节3同样处理。
        假定当前u32 rt_thread_priority_bitmap维护着当前系统优先级位图。

       现在我们解决了32个系统优先级时的调度问题,现在来考虑线程优先级为256的情况。读者可能会想了,这没什么不同,256个bit=32个字节,依然采用算法3的思路,对着32个字节依次查表。问题是,当位图变量有32个字节时,对这32个字节依次查表耗费的时间就不可以忽略了,为了提升系统实时调度的性能,我们需要对算法3进行改进。
       为了解决这个问题,我们使用二级位图。即,256个bit由32个字节存储,每一个字节的8个bit代表着位图变量中的8个优先级,如果某个字节非0,则表示其中必有非0的bit位。
       rtt中对应的数组为rt_uint8_t rt_thread_ready_table[32]
       所谓二级位图,即我们先确定32个字节中最低的非0的字节。为了实现这个效果,我们需要对这32个字节引入一个32个bit的位图变量,每一个bit位表示对应的字节是否为0。例如,这个32bit的位图变量的BIT5为0,表示系统线程优先级256bit所分成的32个字节中的 字节5 非0。 为了区分,称这个32个bit的位图变量 字节位图变量 ,rt-thread中使用的是rt_thread_ready_priority_group. 显然我们查找系统系统最高优先级时,先确定非0的最低字节,这实际上依然是算法3,然后再对该字节进行查表,即得到该字节内最低为1的bit位,然后两者叠加(注意不是简单的加)即可。
       根据上面的分析,要想使用这个二级位图算法,rtt在跟踪线程的状态转换时,不仅需要维护256bit的位图变量数组rt_thread_ready_table[thread->number] |= thread->high_mask,还需要维护32bit的 字节位图变量 rt_thread_ready_priority_group。参看如下代码。
μC/OS-II与RT-Thread对比——任务调度_第8张图片
       初始化线程时,我们指定了一个线程的优先级别thread->init_priority,由于线程优先级为0到255,一个字节就可以表示。但是我们的bitmap是32个字节。为了调高效率,我们最好能快速向位图的对应的bit写1。
       语句(1)thread->current_priority >> 3,这里的>>3就是除以8,因为一个字节表示8个优先级。这样就可以得到当前这个优先级对应的位图32个字节中的第几个字节,这里用thread->number表示,显然,number范围是0到31。这里为了提高效率,采用移位完成除法。
       上面除法的余数,就表示这个优先级在上面字节中的第几个bit。这个余数可以使用 (thread->current_priority & 0x07)来表示。
       语句(3)是得到该bit对应的权值。例如一个字节的bit7对应的权值即 (1<<7),这样做是为了使用“位与,或,非”等位运算,可以提高运行速度,即语句(4)。
       语句(4)清楚表示了这几个变量作用。可见,根据某个表示优先级的数字向位图中相应的bit位写入了1。
       那么语句(2)和(5)是做什么用的呢? 这个number_mask实际上是为了加快查找位图的速度而创建的。它将在rt_schedule函数中发挥作用。
      上文已说明,thread->number就表示当前线程优先级在32个字节的位图数组中的字节位置。为了提高效率,rt-thread另外使用了一个u32类型的变量rt_thread_ready_priority_group 来加快速度。如果这32个bit中某一个bit为1,就表示对应的某个字节非0(想想看,这意味着该字节所表示的8个优先级中存在就绪线程)。
       rt_thread_ready_priority_group变量为32位宽度,长度上等于4个字节,因此可以对每一个字节查表(上面生成的表格)就可以得到为1的最低的bit位置。
       概括起来就是,rtt首先确定32个字节的位图中,非0的最低的那个字节,然后再查表得到这个字节非0的最低那个bit。这两步骤正好可以利用两次上面的表格rt_lowest_bitmap。
      下面附上rt_schedule的代码。非必要的代码被我隐去。读者可以对比下面的代码理解上面的思路。
μC/OS-II与RT-Thread对比——任务调度_第9张图片

4. 总结

       从上面的对比中可以看出, RT-Thread与μC/OS-II在任务调度方面的算法基本一致,复杂度都为O(1),但是前者通过二级位图支持更多的优先级,同时在支持优先级相同的任务采用时间片轮转的方式进行调度。

参考文献:
1.《RT-Thread编程手册》
2.《嵌入式实时操作系统uCOS-II》
3. rt-thread的位图调度算法分析:http://blog.csdn.net/hcx25909/article/details/18009535
4. UCOS之任务调度机制 : http://tianwaike1.blog.163.com/blog/static/351366792009911101830325/

----------------------------------------------------------------

欢迎大家转载我的文章。

转载请注明:转自古-月

http://blog.csdn.net/hcx25909

欢迎继续关注我的博客


你可能感兴趣的:(μC/OS-II与RT-Thread对比——任务调度)