4. 任务调度算法:抢占式调度、时间片轮转、协作式调度、调度器启动与挂起、调度策略选择
调度算法,说白了就是RTOS的「大脑」。它决定了哪个任务能跑、跑多久、什么时候换人。我做了这么多年嵌入式,见过太多系统因为调度策略选错而翻车。今天咱们就把这块掰开揉碎了讲清楚。
4.1 抢占式调度:实时系统的基石
抢占式调度,是现在绝大多数RTOS的默认选择。它的核心思想很简单:高优先级的任务可以随时打断低优先级任务的执行。
你想想看,如果系统里有个紧急任务要处理温度报警,它必须立刻响应。这时候如果还在慢悠悠地跑着显示任务,那设备可能就烧了。抢占式调度就是为了解决这种问题。
核心机制:每次系统时钟中断(tick interrupt)到来时,调度器都会检查当前运行任务的优先级。如果就绪队列中有更高优先级的任务,就立即进行上下文切换。
我在项目中遇到过这样一个坑:有个同事把两个任务设成了相同优先级,结果高优先级任务一直抢占不到资源,因为调度器只在tick中断时才检查。嗯,这里要注意——抢占式调度依赖时钟中断,中断频率决定了响应速度。
// 典型的抢占式调度实现(简化版)
void scheduler_tick_handler(void) {
// 保存当前任务上下文
save_context(current_task);
// 检查是否有更高优先级任务就绪
task_t *next = get_highest_priority_ready_task();
if (next != current_task) {
// 执行上下文切换
context_switch(current_task, next);
current_task = next;
}
// 恢复新任务上下文
restore_context(current_task);
}
4.2 时间片轮转:公平的「分时」艺术
时间片轮转,说白了就是让同优先级的任务轮流跑。每个任务分到一个时间片(比如10ms),时间到了就换下一个。
为什么要用时间片?我举个例子:系统里有三个任务,都是中等优先级,分别处理按键扫描、LED闪烁、数据采集。如果不用时间片,一个任务可能霸占CPU直到它自己主动让出。其他任务就饿死了。
| 调度方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 抢占式 | 实时性要求高 | 响应快 | 优先级反转风险 |
| 时间片轮转 | 同优先级任务多 | 公平、无饥饿 | 上下文切换开销大 |
| 协作式 | 任务简单、资源少 | 开销小、可预测 | 一个任务卡死全系统 |
我的经验:时间片长度一般设为系统tick周期的1-5倍。太短了频繁切换浪费CPU,太长了又失去公平性。我习惯用10ms,大多数场景都够用。
4.3 协作式调度:简单但危险
协作式调度,也叫非抢占式调度。任务自己决定什么时候让出CPU。说白了就是「你跑完了我再跑」。
这种调度方式在早期的嵌入式系统里很常见。为什么?因为实现简单,不需要时钟中断,也不需要复杂的上下文切换。资源极度受限的单片机(比如4KB RAM)上,协作式调度是唯一选择。
但是——协作式调度有个致命问题:一个任务如果写了个死循环,整个系统就挂了。我曾经接手过一个项目,前任工程师用协作式调度,结果有个任务里while循环忘了加延时,系统每隔几分钟就卡死一次。排查了整整两天才找到问题。
// 协作式调度:任务主动让出CPU
void task_a(void) {
while(1) {
// 做点工作
do_something();
// 主动让出CPU
task_yield();
}
}
void task_b(void) {
while(1) {
// 做点工作
do_other_thing();
// 主动让出CPU
task_yield();
}
}
避坑指南:我曾经在协作式调度系统里犯过一个错——任务A里调用了阻塞式I/O操作,结果任务B永远拿不到CPU。后来我强制要求:协作式调度下,所有I/O操作必须是非阻塞的,或者加上超时机制。
4.4 调度器启动与挂起
调度器的启动和挂起,是RTOS初始化和运行时控制的关键环节。很多新手在这里翻车。
调度器启动:一般在系统初始化完成后调用。启动后,调度器开始管理任务切换。注意——启动调度器前,必须确保至少有一个任务处于就绪状态,否则系统会直接崩溃。
void main(void) {
// 硬件初始化
hardware_init();
// 创建任务
task_create(task_a, "TaskA", 1024, 3);
task_create(task_b, "TaskB", 1024, 2);
// 启动调度器(不会返回)
scheduler_start();
// 永远不会执行到这里
while(1);
}
调度器挂起:有时候我们需要临时关闭调度,比如在临界区操作共享资源时。挂起调度器后,任务切换被禁止,当前任务独占CPU。
关键点:调度器挂起的时间必须尽可能短。我见过有人把调度器挂起后做了一堆复杂计算,结果高优先级任务错过了响应窗口。一般来说,挂起时间不要超过100微秒。
// 挂起和恢复调度器
void critical_section_enter(void) {
scheduler_suspend(); // 挂起调度器
// 现在可以安全操作共享资源
}
void critical_section_exit(void) {
// 操作完成
scheduler_resume(); // 恢复调度器
// 注意:恢复后可能立即发生上下文切换
}
4.5 调度策略选择:没有银弹
选哪种调度策略,取决于你的应用场景。我个人的经验是:
- 硬实时系统(比如汽车ECU、工业控制器):必须用抢占式调度。响应时间确定性是第一位的。
- 软实时系统(比如智能家居网关、数据采集器):抢占式+时间片轮转混合使用。高优先级任务抢占,同优先级任务轮转。
- 超低功耗系统(比如电池供电的传感器):协作式调度。没有时钟中断,功耗更低。
- 资源极度受限(比如4KB RAM的MCU):协作式调度。抢占式调度的上下文切换开销太大。
我的建议:如果你不确定选哪种,默认用抢占式调度。大多数RTOS(FreeRTOS、uC/OS、RT-Thread)默认就是抢占式。等系统跑起来后,再根据实际性能数据调整。
最后说一句:调度策略没有绝对的好坏。我见过用协作式调度跑得很稳的工业设备,也见过抢占式调度翻车的消费电子产品。关键是你得理解每种策略的优缺点,然后根据你的硬件资源和实时性要求做选择。