实验平台:stm32f10x(cortex-m3)开发板,RTT3.0
资料来源:RTT官网文档及cortex-M3权威指南
关键字:分析RT-Thread源码、stm32编写一个简单的RTOS、调度器。
额,这里还是介绍一下调度的功能吧(瞬间打脸)。
调度的作用就是从一堆当前需要运行的线程中找到那个最需要马上运行的程序。然后通过上下文切换,将cpu的控制权移交给该线程。上一节我们已经了解并完成了上下文切换功能,所以这一节主要就是介绍如何找到那个最需要马上运行的线程,但也要保证各个线程尽可能的公平共享cpu时间。而且应该保证调度花费的时间尽量的小,不然实时性就会降低。所以对调度器就有如下要求:
1):响应快。不能因线程的增多就导致调度时间变长,所以要求调度算法的时间复杂度要为O(1)。
2):实时性。当系统发生突发事件时,如某个线程需要立刻运行,调度器能让其马上得到cpu控制权。所以调度器还得支持可抢占的。
RT-Thread中提供的线程调度器是基于优先级的全抢占式调度:在系统中除了中断处理函数、调度器上锁部分的代码和禁止中断的代码是不可抢占的之外,系统的其他部分都是可以抢占的,包括线程调度器自身。系统总共支持256个优先级。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。RT-Thread内核中也允许创建相同优先级的线程。相同优先级的线程采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪线程存在的情况下才有效。
RT-Thread内核中采用了基于位图(bitmap)的优先级算法(时间复杂度O(1),即与就绪线程的多少无关),通过位图的定位快速的获得优先级最高的线程。
如果你看过编程珠玑,你应该对位图算法有一定的了解,记得这是第一章的内容吧,用位图算法来排序号码。下面介绍一下这个算法。
看名称我们就可以知道这个算法和位(bit)有关系,位图排序算法和这里的查找算法其实都是一个原理,都是通过置位来标记,通过一定的规则算出最高优先级号。下面就直接举例子了。
我们知道一个字节有8bit,范围从0-255。假设现在有两个不同的优先级5(低)和3(高优先级)。
我们将按照不同的优先级将1进行左移,即优先级5:1 << 5,优先级3:1 << 3。将优先级都保存到一个变量rt_thread_ready_priority_group上。即:
rt_thread_ready_priority_group |= 1 << 5; //0010 0000
rt_thread_ready_priority_group |= 1 << 3; //0010 1000
假设数字越小优先级越大,可以看到,我们只需要查找rt_thread_ready_priority_group最低位为1的位置就是我们要找的最高优先级了。8个bit有256种不同的结果。如果我们提前将其算好,如上的值为0x28,array[28] = 3,我们马上就可以知道最高的优先级是哪一个了,而且时间复杂度为O(1),这个就是典型的空间换时间了。
rt_thread_ready_priority_group有32bit,所以可以保存32个不同的优先级了。但是当优先级数超过32时,怎么办?我们的单片机也就32bit?原理其实还是一样的,RTT采用了二级位图来解决这个问题。即一个不够记那么我们就用多个来记。因为数组的地址刚好是连续的,所以我们可以用数组来记录。如果我们规定的最大优先级是256的话,那么32*8=256,所以我们只需要32个字节就够了。但是如果只有一个数组的话,我们每次都要从0下标的数组元素开始扫描,如果我们只有一个优先级255,那么我们可能得扫描到数组array[31]才能得到最高优先级,这样花费的时间就有点多了。前面我们已经说了,RTT是采用二级位图来解决这个问题的,其实方法还是一样,我们用一个变量来标记哪些数组下标是有存储优先级的,这样我们就能直接找到那个最高优先级的数组元素而不用去轮询了。
举个例子:
rt_uint32_t ready_priority_group; //标记哪些数组下标有存储优先级
rt_uint8_t ready_table[32]; //实际记录了优先级的情况
假如现在有个优先级为9的线程。一个数组元素可以存储8个优先级。所以将优先级除以8就知道该存储在那个数组元素了。
ready_priority_group = 9 / 8 = 9 >> 3 = 1; //左右移更快。
ready_table[1] = 9 % 8 = 9 & 0x07 = 1; //先不用或,方便理解
我们就可以通过这两个变量找到具体的优先级了。__rt_ffs假设这个函数可以找到一个变量的最低被置1的位置。那么:
index = __rt_ffs(ready_priority_group)就可以找到哪个数组下标了,
然后__rt_ffs(ready_table[index])就得到了具体的优先级。但是我们还得将index * 8,因为一个数组元素代表8个优先级嘛。
所以highest_ready_priority = index * 8 + __rt_ffs(ready_table[index]);
好了算法就介绍到这,下面跟着代码分析一下:
/* Maximum priority level, 256 */
rt_uint32_t rt_thread_ready_priority_group;
rt_uint8_t rt_thread_ready_table[32];
从线程初始化函数开始,这里设置的线程优先级被保存到了thread->init_priority,current_priority 中。假设我们优先级为123.
static rt_err_t _rt_thread_init (...)
{
...
thread->init_priority = priority; //123
thread->current_priority = priority; //123
...
}
而在rt_thread_startup中进行了如下操作。
rt_err_t rt_thread_startup(rt_thread_t thread)
{
....
thread->number = thread->current_priority >> 3; /* 5bit */ //除以8,number = 15 => table[15]
thread->number_mask = 1L << thread->number; //位15被置1了
thread->high_mask = 1L << (thread->current_priority & 0x07); /* 3bit */ //1L << 3
...
}
在加入调度队列时的处理:
void rt_schedule_insert_thread(struct rt_thread *thread)
{
...
rt_thread_ready_table[thread->number] |= thread->high_mask; //table[15]位3置1
rt_thread_ready_priority_group |= thread->number_mask; //位15被置1了
...
}
接下来我们看在调度的时候是如何得到这个值的。
void rt_schedule(void)
{
...
register rt_ubase_t number;
number = __rt_ffs(rt_thread_ready_priority_group) - 1; //算出最高优先级的数组下标16 - 1
highest_ready_priority = (number << 3) + __rt_ffs(rt_thread_ready_table[number]) - 1; //(15*8) + 4 - 1 = 123
......
}
RTT2.0版本,3.0之后的改为汇编了,为了方便理解,这里贴2.0的。
因为__lowest_bit_bitmap是256个字节的,即8bit,所以需要将32bit分为4段,所以需要n(1~4)*8。
int __rt_ffs(int value)
{
if (value == 0) return 0;
if (value & 0xff)
return __lowest_bit_bitmap[value & 0xff] + 1;
if (value & 0xff00) // bit15 : 0x8000 >>8 = 0x80 = 128
return __lowest_bit_bitmap[(value & 0xff00) >> 8] + 9; //7 +9 = 16
if (value & 0xff0000)
return __lowest_bit_bitmap[(value & 0xff0000) >> 16] + 17;
return __lowest_bit_bitmap[(value & 0xff000000) >> 24] + 25;
}
所以就这样算出最高优先级了。其实并不难理解,不过我写的好像有点乱七八糟,反而有点懵逼。。
今天就先摸鱼到这里。。
现在我们找到了最高优先级,要怎么用最高优先级找到具体的线程呢?这个时候我们需要用到双链表的知识,所以这块还不熟悉的可能理解起来会有困难。这里我就不讲链表相关的知识了,篇幅有限。
我们先来看一下RTT是怎么通过优先级找到线程的句柄。如下:
void rt_schedule(void)
{
...
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;
/* get switch to thread */
to_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next,
struct rt_thread,
tlist);
...
}
这里说一下rt_list_entry的作用:就是通过结构体的成员地址去算结构体的起始地址。是的,就是linux中的container_of宏。关于container_of网上已经介绍的十分详细,这里也不再讲解,自行百度。我们这里只关心一下数组rt_thread_priority_table,这是一个链表数组,即数组的元素是由链表组成的。其定义如下:
struct rt_list_node
{
struct rt_list_node *next; /< point to next node. */
struct rt_list_node *prev; /< point to prev node. */
};
typedef struct rt_list_node rt_list_t; /**< Type for lists. */
rt_list_t rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];
我们看一下它和线程又有什么关系。
首先在rt_system_scheduler_init中初始化了该数组,目前还没有和线程有什么关联。
void rt_system_scheduler_init(void)
{
...
for (offset = 0; offset < RT_THREAD_PRIORITY_MAX; offset ++)
{
rt_list_init(&rt_thread_priority_table[offset]);
}
...
}
它是在什么时候和线程关联上的呢?我们看一下 rt_thread_startup。
rt_err_t rt_thread_startup(rt_thread_t thread)
{
...
/* change thread stat */
thread->stat = RT_THREAD_SUSPEND; //修改线程状态为挂起状态
/* then resume it */
rt_thread_resume(thread); //恢复线程
if (rt_thread_self() != RT_NULL)
{
/* do a scheduling */
rt_schedule(); //调度
}
return RT_EOK;
}
rt_err_t rt_thread_resume(rt_thread_t thread)
{
...
/* insert to schedule ready list */
rt_schedule_insert_thread(thread);
...
}
void rt_schedule_insert_thread(struct rt_thread *thread)
{
...
/* change stat */
thread->stat = RT_THREAD_READY; //修改为就绪状态,只有就绪才能转入运行状态。
/* insert thread to ready list */ //插入链表中,这是线程就和rt_thread_priority_table关联了。
rt_list_insert_before(&(rt_thread_priority_table[thread->current_priority]),
&(thread->tlist));
...
}
在rt_thread_resume中调用了rt_schedule_insert_thread,此时线程就和rt_thread_priority_table联系上了。
rt_thread_priority_table[priority]就是存储已经就绪的线程链表。
所以我们通过thread的成员tlist就可以用rt_list_entry/container_of找到线程的句柄了。
/* get switch to thread */
to_thread = rt_list_entry(rt_thread_priority_table[highest_ready_priority].next,
struct rt_thread,
tlist);
找到句柄我们就可以用上下文切换函数进行任务的切换了。
rt_hw_context_switch((rt_uint32_t)&from_thread->sp,
(rt_uint32_t)&to_thread->sp);
1:调度器初始化
2:将就绪状态的线程插入就绪优先级表中
3:找到最高优先级号(利用位图来记录就绪优先级存储和预先算好8bit的所有可能值的最低位)
4:用优先级号找到线程句柄
5:进行上下文切换
关于调度就介绍到这结束了。