任务调度机制详解
调度器,说白了就是操作系统的「大脑」。它决定哪个任务该运行,哪个任务该等着。我刚开始接触 RT-Thread 时,总觉得调度是个很玄乎的东西。后来自己动手移植了一遍,才明白它其实就是一套规则加一套切换机制。
这一章,我们就把调度器的底裤扒开看看。
调度器工作原理
RT-Thread 的调度器核心逻辑,其实就一句话:谁优先级高谁先跑,同等优先级轮流跑。
调度器内部维护了一个就绪任务列表。每次调度发生时,它从列表里挑出优先级最高的任务,然后切过去执行。这个动作,我们叫「任务切换」。
核心要点:调度器本身不是一个独立的任务,它是由一系列函数调用触发的。换句话说,调度器是被「叫醒」的,不是自己在那一直转。
我个人习惯把调度器的工作流程拆成三步:
- 查表 —— 从就绪优先级位图中找到最高优先级的任务
- 取任务 —— 从对应优先级的就绪队列中取出任务控制块
- 切换 —— 保存当前任务上下文,恢复新任务的上下文
嗯,这里要注意:查表这一步,RT-Thread 用了位图算法,速度非常快。不管系统里有多少个任务,查最高优先级的时间都是固定的。这一点在实时系统中很关键。
基于优先级的抢占式调度
抢占式调度,是 RT-Thread 的默认模式。它的规则很简单:只要有一个更高优先级的任务就绪了,当前任务就得让位。
我在项目中遇到过一个问题:一个低优先级任务正在写 Flash,突然被高优先级任务抢占了。结果高优先级任务也要写 Flash,两个任务同时操作 Flash 控制器,数据全乱了。这就是抢占带来的资源竞争问题。
避坑指南:我曾经在一个电机控制项目里,把中断优先级和任务优先级搞混了。中断里释放信号量唤醒高优先级任务,结果高优先级任务抢占了中断返回后的低优先级任务,导致低优先级任务的时序完全乱掉。记住:中断返回后一定会触发一次调度,这是抢占的关键时机。
抢占式调度的触发时机,主要有这几个:
| 触发点 | 说明 |
|---|---|
| 任务创建/恢复 | 新任务就绪,可能比当前任务优先级高 |
| 任务释放资源 | 如释放信号量、消息队列,可能唤醒高优先级任务 |
| 任务主动挂起 | 当前任务让出 CPU,调度器选下一个 |
| 中断返回 | 中断里可能让高优先级任务就绪 |
| 时间片用完 | 同等优先级任务轮转 |
时间片轮转调度
你想想看,如果两个任务优先级一样,谁先跑?答案是:谁先来谁先跑,但每人只能跑一个时间片。
时间片轮转,就是解决同等优先级任务之间的公平问题。每个任务可以连续运行的时间,叫做一个时间片。时间片用完了,调度器就把这个任务放到队列尾部,让下一个任务上。
RT-Thread 的时间片单位是系统时钟节拍(tick)。默认一个 tick 是 1 毫秒,但你可以改。我个人习惯把时间片设成 5~10 个 tick,太短了切换开销大,太长了响应不及时。
经验之谈:时间片不是越大越好。我做过一个数据采集项目,三个同等优先级的任务分别负责采集、处理、发送。时间片设成 50ms,结果采集任务每次都要等 100ms 才能再次运行,数据丢包严重。后来改成 5ms,问题解决。
时间片轮转的代码逻辑,其实就在调度器里:
// 简化版时间片处理逻辑
if (当前任务的时间片 > 0) {
当前任务的时间片--;
} else {
// 时间片用完,移到队列尾部
rt_list_remove(&当前任务->tlist);
rt_list_insert_before(&就绪队列, &当前任务->tlist);
// 重置时间片
当前任务->remaining_tick = 当前任务->init_tick;
// 触发调度
需要调度 = 1;
}
调度点与上下文切换
调度点,就是调度器被触发执行的地方。RT-Thread 的调度点分布在系统的各个关键位置。
我画了一张图,帮你理清调度点和上下文切换的关系:
上下文切换,是调度器最底层的操作。它由汇编代码完成,主要做两件事:
- 保存现场:把当前任务的 CPU 寄存器(R4-R11、PC、LR、PSP 等)压入当前任务的栈
- 恢复现场:从新任务的栈里弹出寄存器值,然后跳转到新任务继续执行
说白了,上下文切换就是「换栈」。每个任务有自己的栈空间,切换时只要把栈指针(PSP)一换,CPU 就跑到了另一个任务的执行路径上。
调试技巧:我调试任务切换问题时,最喜欢在 PendSV_Handler 里加断点。每次触发 PendSV 中断,就说明要发生上下文切换了。看栈指针的变化,就能判断切换是否正常。
空闲线程与钩子函数
空闲线程,是 RT-Thread 自动创建的、优先级最低的线程。它的优先级是 255(RT-Thread 里优先级数值越大,优先级越低)。
空闲线程平时干什么?啥也不干,就在那死循环。但它的存在有两个重要意义:
- 占位:保证就绪队列里永远至少有一个任务可运行
- 回收:当任务被删除时,空闲线程负责回收任务占用的资源
空闲线程的钩子函数,是 RT-Thread 提供的一个扩展机制。你可以注册一个函数,让空闲线程每次循环时调用它。
// 注册空闲线程钩子函数
void my_idle_hook(void)
{
static uint32_t count = 0;
count++;
// 可以在这里做低功耗处理
// 或者统计CPU空闲率
}
rt_thread_idle_sethook(my_idle_hook);
我在项目中用过空闲钩子做两件事:
- CPU 空闲率统计:在钩子里累加计数器,通过计数器值估算 CPU 负载
- 低功耗模式:当系统空闲时,让 MCU 进入睡眠模式,等中断唤醒
注意:空闲钩子函数里不能调用会导致任务挂起的函数(如 rt_thread_delay、获取信号量等)。因为空闲线程本身不能挂起,一旦挂起系统就没人兜底了。我曾经犯过这个错,在钩子里调了 rt_thread_mdelay,结果系统直接死机。
空闲线程的代码逻辑,其实很简单:
// 空闲线程的简化实现
void rt_thread_idle_entry(void *parameter)
{
while (1) {
// 执行所有注册的钩子函数
for (i = 0; i < idle_hook_count; i++) {
idle_hook_list[i]();
}
// 回收已删除任务的资源
rt_thread_defunct_reclaim();
}
}
嗯,到这里,调度器的核心机制就讲完了。从调度器怎么选任务,到抢占怎么发生,再到时间片怎么轮转,最后是空闲线程怎么兜底。这些知识,是理解 RT-Thread 运行机制的基础。