源码解读·RT-Thread多任务调度算法

*本文依据RT-Thread当时最新版本4.0.1版本源码

RT-Thread操作系统是一款基于优先级和时间片轮转的多任务实时操作系统。其调度算法采用256个优先级,并支持相同优先级的任务存在。不同优先级的任务采用优先级调度,而相同优先级的任务则采用时间片轮转调度。其实这种调度算法在绝大多数系统中都一样,像我知道的μCos和freertos都是如此。不过这里需要先了解一个问题,也是我初学时被困扰的问题——多种调度算法存在时那么何时采用何种调度算法?彼此又是如何共存和协调进行的?这要等看完并看懂调度算法的源码之后才算明白其中原理。其实调度算法采用优先级调度为主要依据,以时间片轮转为次要依据。也就是说只当没有更高优先级任务就绪的情况下,想同优先级任务之间的调度才会采用时间片轮转调度。

优先级调度

优先级在多任务调度中是什么?优先级其实是给任务分配的一个数值,数值越小则优先级越高。优先级的高低将直接反应在任务调度算法中,优先级越高越优先响应。

在RT-Thread中优先级调度算法支持256个优先级,可以通过宏定义配置。通常情况下裁剪过程中会根据需要来定义优先级数量。不过在源码中只会体现出两种不同优先级数量的差异,分别为32个和32个以上。RT-Thread采用bitmap算法来计算优先级。bitmap算法是二进制与位运算完美结合的体现。看懂代码之后相信大家都会来一句“卧槽,既然还可以这样操作!”,真的崇拜发明bitmap算法的大佬,不过我并不知道是谁最先发明的,第一次接触是在学习μCos的时候。

前面提到过,RT-Thread根据优先级的数量不同分为两种bitmap算法(优先级32个和32个以上),代码稍微有些差异,主要是为了优化资源占用。其中不超过32个优先级的情况下只会用一个32bit的变量,超过32个优先级后会使用一个长度为32个元素的byte数组,外加一个32bit的变量用来分组。其中无论多少个优先级,每个优先级都只需要用一个bit来表示对应优先级的任务是否就绪状态(为1表示就绪,为0表示挂起),所以最多支持256个优先级。

bitmap算法

为了理解优先级计算中使用的bitmap算法,首先必须要先掌握十进制与二进制的转换,并且还需要掌握位运算。以RT-Thread中优先级大于32个的情况为例来说明,其32个及一下优先级数量的方式更简单,稍后会简单说明。先看RT-Thread中的源码,关于bitmap算法会用到的几个变量:

rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
struct rt_thread *rt_current_thread;
rt_uint8_t rt_current_priority;

#if RT_THREAD_PRIORITY_MAX > 32
    /* Maximum priority level, 256 */
    rt_uint32_t rt_thread_ready_priority_group;
    rt_uint8_trt_thread_ready_table[32];
#else
    /* Maximum priority level, 32 */
    rt_uint32_t rt_thread_ready_priority_group;
#endif

RT-Thread中当优先级大于32个的情况下,将任务优先级分为32组,每组8个。其分组用rt_thread_ready_priority_group变量来管理,这是一个32bit的变量,每一个bit代表一个组的就绪态。而每一组中的8个优先级则用rt_thread_ready_table数组来管理,其每个元素占用一个8bit大小,同样每个bit代表一个优先级。在源码中优先级从0开始最大到255,所以具体的分组情况就是每8个为一组用一个bit来表示,依次分为32组共需要32个bit来表示分组,也就是rt_thread_ready_priority_group变量。当某一组中对应的优先级任务处于就绪态则对应的bit将被置1.例如优先级为19的任务处于就绪,则根据分组情况其处于第三组(16至23),所以rt_thread_ready_priority_group中的第三bit将置1(第三个bit也就是bit2,因为通常习惯将bit从0开始数).这就在后续的调度算法过程中调度器知道此组中有任务处于就绪了。更进一步的细节是,还需要在rt_thread_ready_table中第三个元素(下标为2)中的第四bit(bit3)置1(因为19在第三组16,17,18,19中位于第四个).这样就准确的标识了唯一的优先级号。

那如果同时有多个组中的任务都处于就绪该怎么计算一个最高优先级呢?如果存在同时处于就绪的任务则对应的分组bit都会在rt_thread_ready_priority_group中置1,且同时在rt_thread_ready_table中对应的组中的bit也会置1.前面提到过优先级越高其优先级编号越小,比如优先级0是最高的优先级,优先级255是最低优先级。这就可以推理得知越小的优先级编号就对应在rt_thread_ready_priority_group越低的bit上。不难看出0至7优先级对应rt_thread_ready_priority_group变量的bit0,同理8至15对应比bit2,16至23对应bit3.等等。现假设优先级5和19的任务处于就绪态,那么其rt_thread_ready_priority_group变量的bit0和bit2将被置1,同时rt_thread_ready_table[0]的bit5以及rt_thread_ready_table[2]的bit3将被置1.如下图所示: 

 

 

如果此时调度器调度时,应该要计算出优先级为5的任务来执行。这其实分了三步来计算的。

首先拿rt_thread_ready_priority_group变量利用ffs函数计算最低位为1的bit是第几个bit。显然这个例子中是第一个bit0位为1,假如这个结果我们用叫index的变量暂存起来,那么index等于1.

第二步利用第一步计算的结果index作为rt_thread_ready_table的索引(索引从0开始为第一个,所以要index-1),即rt_thread_ready_table[0]。再一次做ffs(rt_thread_ready_table[0])计算最低位为1的bit是第几位,显然例子是bit5,假如我们再用个变量offset存储这个值,那么offset等于6。

第三步根据前两步计算出来的index和offset得出最终的最高优先级为(index-1)*8+(offset-1),这里的乘法可以用位运算代替,所以等价于((index-1) <<3) + (offset-1).其真正的RT-Thread源码如下:

register rt_ubase_t highest_ready_priority;

#if RT_THREAD_PRIORITY_MAX <= 32
    highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;

#else
    register rt_ubase_t number;

    number = __rt_ffs(rt_thread_ready_priority_group) -1;
    highest_ready_priority = (number << 3) +__rt_ffs(rt_thread_ready_table[number]) - 1;
#endif

其上面的代码__rt_ffs返回值就如上例的index变量,做了减1操作是因为索引和bit都从0开始。highest_ready_priority即计算出来的最高优先级。

关于ffs函数的实现细节可以看RT-Thread源码里的各种实现方式,有C函数实现,也有针对各种编译器和处理器优化的特殊指令的实现。不过其功能就是计算一个值二进制位为1的最低位是第几位。在RT-Thread的C函数实现中做了一个0到255的索引数组,其数组的值分别就是0到255这些数值所对应的二进制位为1的最低位索引。

最后说明一下,如上面的代码所示,当优先级数定义为不超过32个时,就不存在rt_thread_ready_table了,更节省资源。也可以理解为每组只有一个优先级,所以可以直接用rt_thread_ready_priority_group直接代替了。因为最多才32个优先级,rt_thread_ready_priority_group刚好32bit,每个bit代表一个优先级刚好对应上。

时间片轮转调度

在说明时间片轮转调度前,先要说明一下什么是时间片。在操作系统里,时间片的概念是相对于操作系统的TICK中断的。每触发一次TICK中断就相当于一个时间片。

时间片轮转调度会在每个TICK中断时对当前任务的时间片减一,然后检查其它任务的时间片剩余情况。一旦当前任务的时间片用完,则会先重置当前任务的时间片。然后看是否有想同优先级的任务,如果有则会将当前任务移到队列末尾。然后触发优先级调度,此时只要当前优先级是已就绪的最高优先级最终就会取出相同优先级队列头的任务运行。抛开其它因素简单来说就是只要当前任务的时间片用完了,则会将当前任务移到队列末尾,下一个任务自然而然处于队列头将获得运行。所以这就看起来是每个任务轮流来运行,只是每个任务的运行时间长短不一样而已,这个运行的时间长短就是由时间片指定的。

综上所述,体现时间片轮转调度的前提是建立多个相同优先级的任务。因为时间片轮转调度只会发生在相同优先级的任务之间。否则可以认为系统中只存在优先级调度。

下图展示了三个相同优先级任务的时间片轮转调度运行情况:

 

猜你喜欢

转载自www.cnblogs.com/rocotona/p/11096673.html