本文使用了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的代码部分

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h> /*线程控制块*/
static rt_thread_t Task1 = RT_NULL;
static rt_thread_t Task2 = RT_NULL;
static rt_thread_t Task3 = RT_NULL; /*线程入口函数*/
static void Task1_thread_entry(void* parameter);
static void Task2_thread_entry(void* parameter);
static void Task3_thread_entry(void* parameter); /*钩子函数钩上的函数*/
void task_inithock(rt_thread_t thread)
{
rt_kprintf("%s start\r\n",thread->name);
} void task_suspendhock(rt_thread_t thread)
{
rt_kprintf("%s suspend\r\n",thread->name);
} void task_resumehock(rt_thread_t thread)
{
rt_kprintf("%s resume\r\n",thread->name);
} int main(void)
{
rt_thread_inited_sethook(task_inithock);
rt_thread_suspend_sethook(task_suspendhock);
rt_thread_resume_sethook(task_resumehock); /*创建线程1*/
Task1 = rt_thread_create("task1",
Task1_thread_entry,
RT_NULL,
512,
20,
200); if(Task1 != RT_NULL)
rt_thread_startup(Task1);
else
return -1; /*创建线程2*/
Task2 = rt_thread_create("task2",
Task2_thread_entry,
RT_NULL,
512,
28,
200); if(Task2 != RT_NULL)
rt_thread_startup(Task2);
else
return -1; /*创建线程3*/
Task3 = rt_thread_create("task3",
Task3_thread_entry,
RT_NULL,
512,
22,
200); if(Task3 != RT_NULL)
rt_thread_startup(Task3);
else
return -1;
rt_kprintf("开始执行\r\n");
} static void Task1_thread_entry(void* parameter)//优先级25
{
int i = 0;
while(1)
{
rt_kprintf(" 任务一开始执行 \r\n");
rt_thread_mdelay(1000);
rt_kprintf(" 任务一执行完毕 \r\n"); }
} static void Task2_thread_entry(void* parameter)//优先级28
{
int i = 0;
while(1)
{
rt_kprintf(" 任务二开始执行 r\n");
rt_thread_mdelay(1000);
rt_kprintf(" 任务二执行完毕 \r\n");
}
} static void Task3_thread_entry(void* parameter)//优先级为22
{
while(1)
{
rt_kprintf(" 任务三开始执行 \r\n");
rt_thread_mdelay(1000);
rt_kprintf(" 任务三执行完毕 \r\n");
}
}

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

/* switch to new thread */
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER,
("[%d]switch to priority#%d "
"thread:%.*s(sp:0x%p), "
"from thread:%.*s(sp: 0x%p)\n",
rt_interrupt_nest, highest_ready_priority,
RT_NAME_MAX, to_thread->name, to_thread->sp,
RT_NAME_MAX, from_thread->name, from_thread->sp));
    /* set priority mask */
#if RT_THREAD_PRIORITY_MAX <= 32
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("insert thread[%.*s], the priority: %d\n",
RT_NAME_MAX, thread->name, thread->current_priority));
void rt_schedule_remove_thread(struct rt_thread *thread)
{
register rt_base_t temp; RT_ASSERT(thread != RT_NULL); /* disable interrupt */
temp = rt_hw_interrupt_disable(); #if RT_THREAD_PRIORITY_MAX <= 32
RT_DEBUG_LOG(RT_DEBUG_SCHEDULER, ("remove thread[%.*s], the priority: %d\n",
RT_NAME_MAX, thread->name,
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开始,所以还需要减一,同时我们还可以使用移位的算法,于是程序中看到的关于优先级计算的代码是这样:

#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

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

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

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

  在开启线程的函数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. GDKOI 2021 Day2 TG 总结

    又是爆炸的一天,炸多了本蒟蒻已经习以为常 但今天比昨天整整高了 40 分!!!!却还是没有 100 今天本蒟蒻本想模仿奆佬的打字速度,结果思路混乱让我无法开始 T1 不是吧怎么是期望 dp ,期望值怎 ...

  2. Kubernetes将弃用Docker!与 containerd容器引擎

    时间戳:2022-06-07 20:32:19 星期二 撰写文档参考:(阿良-腾讯课堂)Kubernetes将弃用Docker 参考博客k8s入坑之路(3)containerd容器 container ...

  3. .NET中检测文件是否被其他进程占用

    更新记录 本文迁移自Panda666原博客,原发布时间:2021年7月2日. 一.检测文件是否被进程占用的几种方式 在.NET中主要有以下方式进行检测文件是否被进程占用的几种方式: 通过直接打开文件等 ...

  4. gulp入门第一课你需要注意的

    安装 1.首先确保你已经正确安装了nodejs环境.然后以全局方式安装gulp. npm install -g gulp 2.初始化项目. npm init 3.如果想在安装的时候把gulp写进项目p ...

  5. SAP 实例- 下拉框

    效果图 源代码 REPORT rsdemo_dropdown_listbox . DATA init. TABLES scarr. TABLES spfli. TABLES sflight. TABL ...

  6. WPF第三方控件,只能输入数字型数据

    话不多说,根据最近项目需求,为了减少输入验证等相关代码量,需要此控件 先上效果图 默认样式是这样,自己可以根据需求修改外形,但我更喜欢它自带的简洁版 有人可能会问怎么实现的呢?其实很简单,我们设置它的 ...

  7. DAST 黑盒漏洞扫描器 第六篇:运营篇(终)

    0X01 前言 转载请标明来源:https://www.cnblogs.com/huim/ 当项目功能逐渐成熟,同时需要实现的是运营流程和指标体系建设.需要工程化的功能逐渐少了,剩下的主要工作转变成持 ...

  8. 文本处理工具-vim编辑器的常见用法

    文本编辑工具分类: (1)全屏编辑器: nano(字符编辑器).vi.vim (2)行编辑器: sed:可以逐行改文件 vi编辑器 全名:Visual editor,linux系统自带的文本编辑工具 ...

  9. Mysql错误:The server time zone value is unrecognized or represents more than one time zone

    方法1.修改Mysql的时区为东8区,执行如下命令即可: PS:这种方式每次开机都要配置的 set global time_zone='+8:00' 方法2.配置改成这样的 spring.dataso ...

  10. 游戏启动后提示安装HMS Core,点击取消,未再次提示安装HMS Core(初始化失败返回907135003)

    问题描述 我们国内的华为联运游戏集成华为游戏服务SDK 之后,被审核驳回:在未安装或需要更新华为移动服务(HMS Core)的手机上,提示安装华为移动服务,点击取消,未再次提示安装HMS Core. ...