本文使用了rt-thread自带的钩子函数和显示函数进行了实验,从rt-thread自带的延时函数rt_thread_delay()函数入手,对rt-thread系统的调度器进行分析。主要参考资料是野火的rt-thread手册和rt-thread官方文档,汇编部分的指令是参考的cortex-M3权威参考手册,实验版本是rt-thread3.1.5

1、实验准备

1.使用三个线程,内部调用延时函数,每个线程内部延时1s。

2.使用系统自带的钩子函数,在调度器实现调度的时候打印线程状态和名称。

3.使用系统自带的调试函数,打印出 to thread 和 from thread 的名称与优先级以及remove thread 和 insert thread。

主要代码如下

main.c的代码部分

  1. #include <rtthread.h>
  2. #include <rtdevice.h>
  3. #include <board.h>
  4. /*线程控制块*/
  5. static rt_thread_t Task1 = RT_NULL;
  6. static rt_thread_t Task2 = RT_NULL;
  7. static rt_thread_t Task3 = RT_NULL;
  8. /*线程入口函数*/
  9. static void Task1_thread_entry(void* parameter);
  10. static void Task2_thread_entry(void* parameter);
  11. static void Task3_thread_entry(void* parameter);
  12. /*钩子函数钩上的函数*/
  13. void task_inithock(rt_thread_t thread)
  14. {
  15. rt_kprintf("%s start\r\n",thread->name);
  16. }
  17. void task_suspendhock(rt_thread_t thread)
  18. {
  19. rt_kprintf("%s suspend\r\n",thread->name);
  20. }
  21. void task_resumehock(rt_thread_t thread)
  22. {
  23. rt_kprintf("%s resume\r\n",thread->name);
  24. }
  25. int main(void)
  26. {
  27. rt_thread_inited_sethook(task_inithock);
  28. rt_thread_suspend_sethook(task_suspendhock);
  29. rt_thread_resume_sethook(task_resumehock);
  30. /*创建线程1*/
  31. Task1 = rt_thread_create("task1",
  32. Task1_thread_entry,
  33. RT_NULL,
  34. 512,
  35. 20,
  36. 200);
  37. if(Task1 != RT_NULL)
  38. rt_thread_startup(Task1);
  39. else
  40. return -1;
  41. /*创建线程2*/
  42. Task2 = rt_thread_create("task2",
  43. Task2_thread_entry,
  44. RT_NULL,
  45. 512,
  46. 28,
  47. 200);
  48. if(Task2 != RT_NULL)
  49. rt_thread_startup(Task2);
  50. else
  51. return -1;
  52. /*创建线程3*/
  53. Task3 = rt_thread_create("task3",
  54. Task3_thread_entry,
  55. RT_NULL,
  56. 512,
  57. 22,
  58. 200);
  59. if(Task3 != RT_NULL)
  60. rt_thread_startup(Task3);
  61. else
  62. return -1;
  63. rt_kprintf("开始执行\r\n");
  64. }
  65. static void Task1_thread_entry(void* parameter)//优先级25
  66. {
  67. int i = 0;
  68. while(1)
  69. {
  70. rt_kprintf(" 任务一开始执行 \r\n");
  71. rt_thread_mdelay(1000);
  72. rt_kprintf(" 任务一执行完毕 \r\n");
  73. }
  74. }
  75. static void Task2_thread_entry(void* parameter)//优先级28
  76. {
  77. int i = 0;
  78. while(1)
  79. {
  80. rt_kprintf(" 任务二开始执行 r\n");
  81. rt_thread_mdelay(1000);
  82. rt_kprintf(" 任务二执行完毕 \r\n");
  83. }
  84. }
  85. static void Task3_thread_entry(void* parameter)//优先级为22
  86. {
  87. while(1)
  88. {
  89. rt_kprintf(" 任务三开始执行 \r\n");
  90. rt_thread_mdelay(1000);
  91. rt_kprintf(" 任务三执行完毕 \r\n");
  92. }
  93. }

打印函数,只需要将宏定义RT_DEBUG_SCHEDULER打开即可使用

  1. /* switch to new thread */
  2. RT_DEBUG_LOG(RT_DEBUG_SCHEDULER,
  3. ("[%d]switch to priority#%d "
  4. "thread:%.*s(sp:0x%p), "
  5. "from thread:%.*s(sp: 0x%p)\n",
  6. rt_interrupt_nest, highest_ready_priority,
  7. RT_NAME_MAX, to_thread->name, to_thread->sp,
  8. RT_NAME_MAX, from_thread->name, from_thread->sp));
  1. /* set priority mask */
  2. #if RT_THREAD_PRIORITY_MAX <= 32
  3. RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
  4. RT_NAME_MAX, thread->name, thread->current_priority));
  1. void rt_schedule_remove_thread(struct rt_thread *thread)
  2. {
  3. register rt_base_t temp;
  4. RT_ASSERT(thread != RT_NULL);
  5. /* disable interrupt */
  6. temp = rt_hw_interrupt_disable();
  7. #if RT_THREAD_PRIORITY_MAX <= 32
  8. RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("remove thread[%.*s], the priority: %d\n",
  9. RT_NAME_MAX, thread->name,
  10. thread->current_priority));

还有一些打印的函数就不一一展示了,有兴趣的可以翻一翻<rtdebug.h>这个头文件看看里面的宏定义,尝试调一调。

2、实验现象

  可以看到,系统在最开始的时候,首先执行的初始线程,会执行main函数,main函数的优先级值设置的是(最大优先级/3),如果main函数里面有比初始线程优先级更高的优先级的时候,会发生抢占,先执行优先级更高的任务,这与RT_thread的系统启动过程能对的上(系统启动的时候先创建一个初始线程,在初试线程中开启其他任务线程,开启完毕之后系统删除初始线程)。

  从串口打印出来的消息可以看出来,程序初始化完毕后,task1,task2,task3都进入就绪态,系统首先执行优先级最高的task1,当task1执行rt_thread_delay()函数,task1线程进入阻塞态,task1被挂起,调度器执行就绪队列中优先级较高的task3,执行task3中也遇到delay()函数,task3也被挂起,调度器执行task2,task2也执行到delay()函数,进入阻塞态,此时就绪队列中只剩下系统自带的tidle空闲线程。系统开始执行tidle,它的优先级为31,是最低优先级,等先前的task1,2,3这三个任务哪一个执行完毕,退出阻塞态,进入就绪态,线程会立刻切换。

3、代码部分

rt_thread_delay函数

  因为rt_thread_mdelay()函数里面存在调度器调度的函数,所以本次调度器的运行过程,从rt_thread_mdelay()开始分析。

  在RT_thread函数中,定时器控制模块的设计很有意思,在系统中会创建一个定时器链表rt_timer_list,系统所有新创建并且激活的定时器都会以超时时间排序的方式插入到定时器链表中。具体的解释我觉得可能是源码和官方文档解释的最为清楚。下面就是官方文档的一些解释。

rt_thread标准版文档 定时器部分

代码分析

  1、可以看到,当任务一开始执行的时候,程序进入任务中的rt_thread_mdelay()函数rt_thread_mdelay()函数先关闭中断,挂起当前线程,开始计时,打开中断,进入到调度器中。

  2、进入调度函数后,程序首先关闭中断,之后在就绪队列中寻找最高优先级的线程。

  判断比较最高优先级主要使用的是位图法进行判断,当优先级小于32位的时候,会用一个32位的变量进行判断,每一个优先级都需要一个bit位来表示对应优先级是否处于就绪态,处于就绪态的时候为1,挂起的时候为0。于是只需要确定最低位置1的位数,即可判断谁是最高优先级。

  当优先级大于32位的时候,此时将0~255个优先级分成32组,每组8个优先级位,分的组数被存在一个32位的变量rt_thread_ready_priority_group中,它的每一个bit表示一个组的就绪态,而它的每一个组的八个优先级被分配到rt_thread_ready_table的数组中进行管理。当进行优先级判断的时候。

  第一步对优先级组里面是否具有就绪态线程进行判断,得出最大优先级组A。

  第二步对最大优先级组里进行bit的判断,得出组内最大优先级线程B,最后便可通过算式得出就绪队列中最大优先级为

highest_ready_priority = A*8+B

  但是由于优先级计算是从0开始,所以还需要减一,同时我们还可以使用移位的算法,于是程序中看到的关于优先级计算的代码是这样:

  1. #if RT_THREAD_PRIORITY_MAX <= 32
  2. highest_ready_priority = __rt_ffs(rt_thread_ready_priority_group) - 1;
  3. #else
  4. register rt_ubase_t number;
  5. number = __rt_ffs(rt_thread_ready_priority_group) - 1; //算出最大优先级组
  6. highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1; //得出最大优先级线程
  7. #endif

  寻找最大优先级的位图算法是通过汇编实现的,主要是将存放优先级的变量180度反转,之后计算出其前导0的数量,通过计算前导0的数量A,就可以得出最高优先级的bit位(A+1),便可得出bit位中最低(1)位出现的位置,即最高优先级。 汇编代码分析如下:

  1. __asm int __rt_ffs(int value)
  2. {
  3. CMP r0, #0x00 //比较优先级是否与0相等,若相等则函数返回,执行完毕。
  4. BEQ exit
  5. RBIT r0, r0 //位反转,旋转180度,最高位变成最低位
  6. CLZ r0, r0 //计算前导0的数量,即从高位开始,出现(1)前的(0)的数量
  7. ADDS r0, r0, #0x01 //加一,前导0的数量加一即为当前最先出现的(1)的位置
  8. exit
  9. BX lr
  10. }

  当然,位图算法很重要的基础是在于将所有线程的优先级存入变量和数组里,之后才能进行数位的比较。

  在开启线程的函数rt_thread_startup()中可以看到,在线程开启的时候,系统就将优先级(大于32)转化成位图算法中的优先级组数和组内的位置数保存下来。

  之后在实现线程插入就绪队列的函数rt_schedule_insert_thread()中将值赋给调度器里面的组数和位置数,这样就算线程进入就绪队列。

  如果函数进入阻塞态,需要将线程移出就绪队列的话,rt-thread使用了将线程移出就绪队列的函数rt_schedule_remove_thread()中将该线程写入调度器优先级表内的数据恢复,这样线程就被移出就绪队列。

  于是可以猜想,将线程插入和移出就绪队列,是否其实质是将线程的优先级放入和移出调度器中的优先级调度的表里,如果优先级调度的表里存在该线程的优先级,则线程有机会运行,只是需要等待自己成为最高优先级线程即可,若线程优先级未放入调度器的优先级调度的表里,则没有机会运行,即被挂起。

  继续往下深入,探究一下线程插入函数rt_schedule_insert_thread()和线程移出函数rt_schedule_remove_thread()在哪里被调用了,因为实际使用的时候,我们并没有直接调用这两个函数对线程的状态今天改变。

  RT_thread的源码显示线程插入函数rt_schedule_insert_thread()在以下三个函数中被调用:

  当线程被移出就绪队列的时候,是通过rt_schedule_remove_thread()实现的,在以下三个函数中被调用 :

  3、线程切换

  寻找到就绪队列中的最高优先级线程之后,程序开始进行线程切换,且第一次线程切换与之后的线程切换的函数有差别,单次切换使用的是rt_hw_context_switch_to()函数里面将异常中断PendSV与该函数挂接,我们这里主要讨论多次切换线程之后的情况,主要通过rt_hw_context_switch()函数完成切换。这部分代码是通过汇编实现,比较难理解,可以参考野火rt_thread的电子书第85页。

  rt_hw_context_switch()这部分代码主要是触发中断,还给rt_interrupt_from_thread和rt_interrupt_to_thread赋值,之后pendSV中断会用上,pendSV在第一次线程切换的函数里面已经进行了设置。值得一提的是rt_hw_context_switch()函数是用汇编代码写的,软件里面无法跳转,大家想阅读这部分代码可以看一下context_rvds.S这个文件

pendSV中断:

  上下文切换:是将上一个线程运行的内容保存到线程栈里,下文切换是将接下来要运行的线程中线程栈的内容加载到CPU中,同时改变PC指针和PSP指针,实现线程的切换。

4、总结

  本次文章关于rt-thread调度器主要重点放在了从就绪队列中取出最高优先级线程,比较难的地方在于位图算法的理解。回过头来看,所谓调度器的使用,简单来说就是一个优先级的插入和移出的问题,当存放线程优先级的变量和数组中存在一个线程的优先级数据时,该优先级就有机会被运行,只需要等待该线程优先级成为就绪队列中的最大优先级,该线程即运行,当线程遇到一些问题,进入阻塞态的时候,代码中实际上也是在操作存放优先级的变量和数组,将该线程的优先级数据移出,它就不可能被运行,于是就被挂起。

关于rt-thread调度器实现的底层代码分析的更多相关文章

  1. 【Cocos2d-x 3.x】 调度器Scheduler类源码分析

    非个人的全部理解,部分摘自cocos官网教程,感谢cocos官网. 在<CCScheduler.h>头文件中,定义了关于调度器的五个类:Timer,TimerTargetSelector, ...

  2. Cocos2d-X3.0 刨根问底(六)----- 调度器Scheduler类源码分析

    上一章,我们分析Node类的源码,在Node类里面耦合了一个 Scheduler 类的对象,这章我们就来剖析Cocos2d-x的调度器 Scheduler 类的源码,从源码中去了解它的实现与应用方法. ...

  3. Nova创建虚拟机的底层代码分析

    作为个人学习笔记分享.有不论什么问题欢迎交流! 在openstack中创建虚拟机的底层实现是nova使用了libvirt,代码在nova/virt/libvirt/driver.py. #image_ ...

  4. HashMap底层代码分析

    public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; //this.loadFactor为加载因子,其值为默认的加载因子常量:DEFAUL ...

  5. HashSet——add remove contains方法底层代码分析(hashCode equals 方法的重写)

    引言:我们都知道HashSet这个类有add   remove   contains方法,但是我们要深刻理解到底是怎么判断它是否重复加入了,什么时候才移除,什么时候才算是包括????????? add ...

  6. Linux 内核调度器源码分析 - 初始化

    导语 上篇系列文 混部之殇-论云原生资源隔离技术之CPU隔离(一) 介绍了云原生混部场景中CPU资源隔离核心技术:内核调度器,本系列文章<Linux内核调度器源码分析>将从源码的角度剖析内 ...

  7. Linux IO Scheduler(Linux IO 调度器)

    每个块设备或者块设备的分区,都对应有自身的请求队列(request_queue),而每个请求队列都可以选择一个I/O调度器来协调所递交的request.I/O调度器的基本目的是将请求按照它们对应在块设 ...

  8. Linux IO 调度器

    Linux IO Scheduler(Linux IO 调度器) 每个块设备或者块设备的分区,都对应有自身的请求队列(request_queue),而每个请求队列都可以选择一个I/O调度器来协调所递交 ...

  9. Linux IO Scheduler(Linux IO 调度器)【转】

    每个块设备或者块设备的分区,都对应有自身的请求队列(request_queue),而每个请求队列都可以选择一个I/O调度器来协调所递交的request.I/O调度器的基本目的是将请求按照它们对应在块设 ...

随机推荐

  1. vue项目经常遇到的Error: Loading chunk * failed

    vue项目随着代码量.业务组件.路由页面等的丰富,出于性能要求考虑不得不使用代码分割技术实现路由和组件的懒加载,这看似没什么问题 当每次通过npm run build构建生产包并部署到服务器后,操作页 ...

  2. ES6 伪数组转真数组

    更新日志 2022年6月13日 发布. 2022年5月19日 笔记迁移到博客. 直接上代码 [...a];

  3. BUUCTF-被嗅探的流量

    被嗅探的流量 提示告知是文件传输的流量,那进去过滤http流量包即可,找到一个upload目录的,并且是post方式即可,追踪http流即可发现flag

  4. ABAP CDS - Language Elements

    The following sections summarize the language elements of the DDL and DCL of the ABAP CDS, arranged ...

  5. python做小游戏——做个马里奥分分钟解决

    一.前言 嗨喽,大家好呀!这里是小熊猫 在你的童年记忆里,是否有一个蹦跳.顶蘑菇的小人已经被遗忘? 马里奥是靠吃蘑菇成长,闻名世界的超级巨星.特征是大鼻子.头戴帽子.身穿背带工作服.还留着胡子.帽子加 ...

  6. 爬虫(9) - Scrapy框架(1) | Scrapy 异步网络爬虫框架

    什么是Scrapy 基于Twisted的异步处理框架 纯python实现的爬虫框架 基本结构:5+2框架,5个组件,2个中间件 5个组件: Scrapy Engine:引擎,负责其他部件通信 进行信号 ...

  7. 如何在.Net Framework应用中请求HTTP2站点

    背景介绍 本文的需求背景是对接苹果公司的推送服务(APNS),苹果在安全方面比较积极,已经严格限制API只支持HTTP2.但是我这里的应用目前仍然是.NET Framework平台,所以必须寻找一种解 ...

  8. 编写可维护的webpack配置

    为什么要构建配置抽离成npm包 通用性 业务开发者无需挂住配置 统一团队构建脚本 可维护性 构建配置合理的拆分 README文档, chan 构建配置管理的可选方案 通过多个配置管理不同环境的构建, ...

  9. 一张图进阶 RocketMQ - 通信机制

    前 言 三此君看了好几本书,看了很多遍源码整理的 一张图进阶 RocketMQ 图片,关于 RocketMQ 你只需要记住这张图!觉得不错的话,记得点赞关注哦. [重要]视频在 B 站同步更新,欢迎围 ...

  10. mybatis-plus时间字段自动填充

    时间代码自动填充的2种方式 数据库方式 将数据库字段create_time和update_time设置CURRENT_TIMESTAMP,create_time字段后面不需要勾选更新,update_t ...