关于rt-thread调度器实现的底层代码分析

2022-10-15,,,,

  本文使用了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调度器实现的底层代码分析的相关教程结束。

《关于rt-thread调度器实现的底层代码分析.doc》

下载本文的Word格式文档,以方便收藏与打印。