4、任务调度算法(上):基于优先级的抢占式调度,时间片轮转调度,如何选择调度策略。
各位同学,咱们今天聊点硬核的。任务调度,说白了就是RTOS的“大脑”。它决定哪个任务先跑,哪个任务后跑。你想想看,一个MCU就一个核,同一时间只能干一件事。那怎么让几十个任务看起来像在“同时”运行?全靠调度算法在背后翻牌子。
我个人习惯把调度算法分成两大类:优先级驱动和时间驱动。今天咱们先啃下这两块硬骨头。
4.1 基于优先级的抢占式调度
这是工业控制领域最常用的调度方式。没有之一。它的核心思想就一句话:谁优先级高,谁先跑;高优先级的来了,低优先级的立刻让路。
我在项目中遇到过好几次这种情况:一个电机控制任务正在跑,突然来了个急停信号。如果用的是非抢占式调度,那得等电机控制任务跑完才能响应急停。电机都转飞了,黄花菜都凉了。所以,抢占式调度在实时性要求高的场合,是必须的。
抢占式调度的核心规则:
- 每个任务都有一个唯一的优先级(数值越小,优先级越高,这是uC/OS和FreeRTOS的惯例)。
- 就绪队列中,优先级最高的任务获得CPU使用权。
- 如果一个更高优先级的任务就绪了,当前任务会被立刻挂起,让出CPU。
- 调度发生在:任务创建、任务阻塞、任务唤醒、中断退出时。
嗯,这里要注意。抢占式调度虽然实时性好,但有个大坑——优先级反转。我后面会专门讲,这里先提一嘴。简单说就是:低优先级任务拿着锁,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢了,高优先级任务反而被“饿死”。
咱们看个简单的代码示例,模拟一下抢占式调度的逻辑:
// 伪代码:抢占式调度核心
void scheduler(void) {
task_t *current_task = get_current_task();
task_t *highest_ready_task = get_highest_ready_task();
// 如果当前任务不是最高优先级的,就换人
if (highest_ready_task->priority < current_task->priority) {
// 保存当前任务上下文
save_context(current_task);
// 恢复新任务上下文
restore_context(highest_ready_task);
// 切换任务
switch_to(highest_ready_task);
}
}
你看,逻辑其实不复杂。但真正实现的时候,临界区保护和中断嵌套才是考验功力的地方。我刚开始写RTOS内核时,就因为在调度器里忘了关中断,结果任务切换时被中断打断,栈指针乱飞,查了两天bug。
避坑指南:我曾经在抢占式调度中犯过一个低级错误——在调度器里直接调用了阻塞函数。结果调度器自己把自己挂起了,整个系统死锁。记住:调度器本身不能阻塞,它必须是“原子”的。
4.2 时间片轮转调度
说完抢占式,咱们聊聊时间片轮转。这个算法,说白了就是“轮流坐庄”。每个任务分到一小段固定的CPU时间,时间到了就换下一个。不管优先级高低,大家机会均等。
你可能会问:那还要优先级干嘛?其实,时间片轮转通常和优先级配合使用。比如:相同优先级的任务之间,用时间片轮转;不同优先级之间,用抢占式。FreeRTOS就是这么干的。
我记得有个做智能家居网关的项目,里面有几个网络协议栈任务,优先级都一样。如果不用时间片轮转,一个任务可能一直霸占CPU,其他任务就饿死了。开了时间片轮转后,每个任务都能分到一点时间,系统响应就平滑多了。
时间片轮转的关键参数:
| 参数 | 说明 | 典型值 |
|---|---|---|
| 时间片长度 | 每个任务一次能跑的最大时间 | 1ms ~ 100ms |
| 系统时钟节拍 | RTOS的心跳,通常由定时器中断产生 | 1ms ~ 10ms |
| 时间片计数 | 每个任务剩余的时间片数 | 通常为1 |
时间片轮转的实现也很直观:
// 伪代码:时间片轮转调度
void tick_handler(void) {
task_t *current = get_current_task();
// 当前任务的时间片减一
current->time_slice--;
// 时间片用完了,换下一个同优先级的任务
if (current->time_slice == 0) {
current->time_slice = DEFAULT_TIME_SLICE;
task_t *next = get_next_ready_task(current->priority);
if (next != NULL && next != current) {
save_context(current);
restore_context(next);
switch_to(next);
}
}
}
这里有个细节:时间片轮转只在相同优先级的任务之间生效。如果来了个更高优先级的任务,那时间片轮转立刻被打断,高优先级任务先跑。等它跑完了,再回来继续轮转。
小技巧:时间片别设得太短。我见过有人设成100us,结果系统大部分时间都在做任务切换,真正干活的时间反而少了。一般来说,时间片是系统时钟节拍的整数倍,1ms到10ms比较合适。
4.3 如何选择调度策略?
好,两种调度算法都讲完了。那问题来了:我到底该用哪一种?
这个问题没有标准答案。但我可以给你几个实用的判断标准:
- 看任务的实时性要求:如果有任务必须在某个时间内响应(比如电机控制、传感器采集),那就必须用抢占式。时间片轮转没法保证响应时间。
- 看任务的优先级分布:如果所有任务优先级都一样,那用时间片轮转就够了。如果优先级差异很大,那抢占式更合适。
- 看任务的CPU占用率:如果有个任务特别耗CPU(比如复杂的数学计算),用时间片轮转可以防止它饿死其他任务。
- 看系统的复杂度:简单的系统,比如就两三个任务,用时间片轮转就挺好。复杂的系统,任务多、优先级多,那必须上抢占式。
我个人的经验是:工业控制领域,90%的场景用抢占式就够了。剩下的10%,比如一些数据采集、日志打印之类的非实时任务,可以放在一个低优先级任务里,然后用时间片轮转让它们公平竞争。
举个例子。我之前做的一个PLC项目:
- 任务1:IO扫描(最高优先级,抢占式)
- 任务2:运动控制(高优先级,抢占式)
- 任务3:通信协议栈(中优先级,抢占式)
- 任务4:人机界面刷新(低优先级,时间片轮转)
- 任务5:日志记录(低优先级,时间片轮转)
你看,高优先级的任务用抢占式保证实时性,低优先级的任务用时间片轮转保证公平性。这就是典型的混合调度策略。
总结一下:
- 抢占式调度:实时性强,适合有严格时间要求的任务。但要注意优先级反转。
- 时间片轮转:公平性好,适合同优先级任务。但实时性无法保证。
- 混合策略:大多数RTOS的默认选择。高优先级抢占,同优先级轮转。
好了,今天的内容就到这儿。下节课咱们接着聊调度算法的下半部分——EDF(最早截止时间优先)和RMS(单调速率调度)。这两种算法在硬实时系统中非常关键,到时候我会结合一个导弹舵机控制的案例来讲。嗯,敬请期待。