*本文依据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算法会用到的几个变量:

  1. rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
  2. struct rt_thread *rt_current_thread;
  3. rt_uint8_t rt_current_priority;
  4.  
  5. #if RT_THREAD_PRIORITY_MAX >
  6. /* Maximum priority level, 256 */
  7. rt_uint32_t rt_thread_ready_priority_group;
  8. rt_uint8_trt_thread_ready_table[];
  9. #else
  10. /* Maximum priority level, 32 */
  11. rt_uint32_t rt_thread_ready_priority_group;
  12. #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源码如下:

  1. register rt_ubase_t highest_ready_priority;
  2.  
  3. #if RT_THREAD_PRIORITY_MAX <=
  4. highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - ;
  5.  
  6. #else
  7. register rt_ubase_t number;
  8.  
  9. number = __rt_ffs(rt_thread_ready_priority_group) -;
  10. highest_ready_priority = (number << ) +__rt_ffs(rt_thread_ready_table[number]) - ;
  11. #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中断时对当前任务的时间片减一,然后检查其它任务的时间片剩余情况。一旦当前任务的时间片用完,则会先重置当前任务的时间片。然后看是否有想同优先级的任务,如果有则会将当前任务移到队列末尾。然后触发优先级调度,此时只要当前优先级是已就绪的最高优先级最终就会取出相同优先级队列头的任务运行。抛开其它因素简单来说就是只要当前任务的时间片用完了,则会将当前任务移到队列末尾,下一个任务自然而然处于队列头将获得运行。所以这就看起来是每个任务轮流来运行,只是每个任务的运行时间长短不一样而已,这个运行的时间长短就是由时间片指定的。

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

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

 感谢各位网友的支持,可以关注我的微信公众号:鹏城码夫   (微信号:rocotona)

源码解读·RT-Thread多任务调度算法的更多相关文章

  1. 源码解读·RT-Thread操作系统从开机到关机

    本篇内容比较简单,但却很繁琐,篇幅也很长,毕竟是囊括了整个操作系统的生命周期.这篇文章的目的是作为后续设计多任务开发的铺垫,后续会单独再抽出一篇分析任务的相关知识.另外本篇文章以单核MCU为背景,并且 ...

  2. 线程本地变量ThreadLocal源码解读

      一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...

  3. [Hadoop源码解读](六)MapReduce篇之MapTask类

    MapTask类继承于Task类,它最主要的方法就是run(),用来执行这个Map任务. run()首先设置一个TaskReporter并启动,然后调用JobConf的getUseNewAPI()判断 ...

  4. 【原】Spark中Job的提交源码解读

    版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...

  5. SDWebImage源码解读之SDWebImageManager

    第九篇 前言 SDWebImageManager是SDWebImage中最核心的类了,但是源代码确是非常简单的.之所以能做到这一点,都归功于功能的良好分类. 有了SDWebImageManager这个 ...

  6. HttpClient 4.3连接池参数配置及源码解读

    目前所在公司使用HttpClient 4.3.3版本发送Rest请求,调用接口.最近出现了调用查询接口服务慢的生产问题,在排查整个调用链可能存在的问题时(从客户端发起Http请求->ESB-&g ...

  7. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  8. java.lang.system 类源码解读

    通过每块代码进行源码解读,并发现源码使用的技术栈,扩展视野. registerNatives 方法解读 /* register the natives via the static initializ ...

  9. AbstractQueuedSynchronizer源码解读

    1. 背景 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)是Doug Lea大师创作的用来构建锁或者其他同步组件(信号量.事件等) ...

  10. AbstractQueuedSynchronizer源码解读--续篇之Condition

    1. 背景 在之前的AbstractQueuedSynchronizer源码解读中,介绍了AQS的基本概念.互斥锁.共享锁.AQS对同步队列状态流转管理.线程阻塞与唤醒等内容.其中并不涉及Condit ...

随机推荐

  1. javascript 获取上一周的时间

    <script type="text/javascript" language="javascript"> //获取系统时间 var LSTR_nd ...

  2. [Scikit-Learn] - introduction

    scikit-learn是一个用于机器学习的 Python 模块,建立在SciPy基础之上. 主要特点: 操作简单.高效的数据挖掘和数据分析 无访问限制,在任何情况下可重新使用 建立在NumPy.Sc ...

  3. 广义逆高斯分布(Generalized Inverse Gaussian Distribution)及修正贝塞尔函数

    1. PDF generalized inverse Gaussian distribution (GIG) 是一个三参数的连续型概率分布: f(x)=(a/b)p/22Kp(ab−−√)xp−1e− ...

  4. 机器学习:DeepDreaming with TensorFlow (三)

    我们看到,利用TensorFlow 和训练好的Googlenet 可以生成多尺度的pattern,那些pattern看起来比起单一通道的pattern你要更好,但是有一个问题就是多尺度的pattern ...

  5. JDBC学习笔记——事务、存储过程以及批量处理

    1.事务                                                                                   1.1.事务的基本概念和使 ...

  6. WPF 控件的内容属性

    WPF的内容属性不应定都是content, 例如TextBlock的内容属性是Text Panel的内容属性是Children ListBox的内容属性是Items

  7. 解决WPF的ScrollViewer在使用触摸屏时,滑到尽头窗口抖动的情况

    原文:解决WPF的ScrollViewer在使用触摸屏时,滑到尽头窗口抖动的情况 wpf的ScrollViewer在触摸条件下 默认在尽头时会有一个窗口一起被拖动的FeedBack,但对用户的交互很不 ...

  8. WMWaire使用FreeNAS硬盘挂载、Raid0

    FreeNAS硬盘挂载.Raid0 发表于2012 年 03 月 28 日由admin 创建成功,FreeBSD的Hardware显示状态 今天,我们将在VMware工具的帮助下,学习“FreeNAS ...

  9. ELINK编程器典型场景之多APP文件下载

    有些应用场合中,单MCU内会采用BootLoader+APP1+APP2的加载模式,程序启动时先进入BootLoader程序,依据设定条件跳转至APPx应用运行:为满足此类需求,设计多达5个程序文件( ...

  10. seq2seq和Transformer

    简单而言,seq2seq由两个RNN组成,一个是编码器(encoder),一个是解码器(decoder).以MT为例,将源语言"我爱中国"译为"I love China& ...