4. 任务调度算法:时间片轮转、优先级抢占调度、协作式调度
调度算法,说白了就是操作系统决定「下一个该谁跑」的规则。我做了这么多年嵌入式,见过不少系统因为调度策略选错而翻车。今天咱们就把三种最经典的调度算法掰开揉碎讲清楚。
4.1 时间片轮转调度(Round-Robin)
时间片轮转,我习惯叫它「大家轮流玩」策略。每个任务分一个固定时间片,时间一到就换人。公平,但未必高效。
核心思想:所有就绪任务排成队列,每个任务运行一个时间片(通常1-100ms),然后挂到队尾,让下一个任务运行。
我在一个智能家居项目里用过这种调度。当时有6个传感器采集任务,每个任务跑10ms时间片。嗯,效果还行,但有个温度传感器偶尔会超时——时间片不够它完成一次完整的数据处理。
// 时间片轮转调度伪代码
void RoundRobin_Scheduler(void) {
while (1) {
Task_t *current = GetNextTaskFromReadyQueue();
if (current != NULL) {
current->remaining_ticks = TIME_SLICE;
// 切换到这个任务
TaskSwitch(current);
// 时间片用完或任务主动让出CPU
if (current->remaining_ticks == 0) {
// 挂到队尾
AppendToReadyQueue(current);
}
}
}
}
| 优点 | 缺点 |
|---|---|
| 公平性好,每个任务都能得到CPU | 实时性差,紧急任务可能被延迟 |
| 实现简单,代码量小 | 上下文切换开销大(时间片越小越明显) |
| 适合同优先级任务 | 不适合有严格截止时间的任务 |
我的经验:时间片大小选系统心跳的整数倍。我一般设10-50ms,太短切换开销大,太长响应变慢。你想想看,如果时间片设1ms,系统每秒光切换就要花掉不少CPU。
4.2 优先级抢占调度(Priority Preemptive)
这个就厉害了。每个任务有优先级,高优先级的任务随时可以打断低优先级的任务。说白了就是「官大一级压死人」。
我曾经在一个无人机飞控项目里用过这种调度。姿态控制任务优先级最高,必须每1ms响应一次。GPS数据采集优先级低一些,晚几十毫秒没关系。如果不用抢占式,飞机早就摔了。
// 优先级抢占调度伪代码
void Preemptive_Scheduler(void) {
while (1) {
Task_t *highest = FindHighestPriorityTask();
if (highest != NULL) {
// 如果当前运行的任务优先级更低,就抢占
if (current_task != NULL &&
current_task->priority < highest->priority) {
// 保存当前任务上下文
SaveContext(current_task);
// 切换到高优先级任务
TaskSwitch(highest);
}
}
}
}
注意:优先级反转是个大坑。我曾经在一个项目中,低优先级任务拿了锁,中优先级任务一直跑,高优先级任务等锁等到超时。最后加了优先级继承协议才解决。
优先级抢占调度有两种常见策略:
- 固定优先级:优先级在创建时定死,不改。适合任务角色明确的系统。
- 动态优先级:优先级可以调整,比如根据任务剩余时间动态提升。更灵活,但实现复杂。
| 特性 | 固定优先级 | 动态优先级 |
|---|---|---|
| 可预测性 | 高 | 低 |
| 实现复杂度 | 低 | 高 |
| 适用场景 | 硬实时系统 | 软实时系统 |
4.3 协作式调度(Cooperative)
协作式调度,也叫非抢占式调度。任务自己决定什么时候让出CPU。说白了就是「自觉排队,不插队」。
我记得早期做的一个8位MCU项目,RAM只有256字节,跑不了复杂的抢占式内核。就用协作式调度,每个任务主动调用task_yield()让出CPU。虽然简单,但够用。
// 协作式调度伪代码
void Cooperative_Scheduler(void) {
while (1) {
Task_t *current = GetNextTaskFromReadyQueue();
if (current != NULL) {
// 运行当前任务,直到它主动让出CPU
TaskSwitch(current);
// 任务主动调用 task_yield() 后回到这里
// 把任务挂到队尾
AppendToReadyQueue(current);
}
}
}
关键区别:协作式调度中,任务不主动让出CPU,其他任务就别想跑。所以每个任务里不能有死循环或长时间阻塞操作。
协作式调度的优缺点很明显:
- 优点:实现极简单,不需要定时器中断,不需要保存/恢复上下文。RAM占用极小。
- 缺点:一个任务写崩了,整个系统就挂了。实时性完全靠任务自觉。
避坑指南:我曾经在一个协作式系统里,有个同事在任务里写了个while(1)等待外设就绪。结果其他任务全卡死了。后来我规定:所有等待操作必须用状态机实现,不能阻塞。
4.4 三种调度算法对比
咱们用一张表总结一下:
| 特性 | 时间片轮转 | 优先级抢占 | 协作式 |
|---|---|---|---|
| 实时性 | 中 | 高 | 低 |
| 公平性 | 高 | 低(高优先级任务霸占CPU) | 中(依赖任务自觉) |
| 实现复杂度 | 中 | 高 | 低 |
| RAM占用 | 中 | 高(每个任务需要独立栈) | 低 |
| 典型应用 | 多用户系统、简单控制 | 实时控制、通信协议栈 | 超小资源MCU、简单循环 |
4.5 实际项目中的选择策略
说实话,实际项目中很少只用一种调度算法。我常用的做法是:
- 关键实时任务用优先级抢占,保证响应时间
- 同优先级批量任务用时间片轮转,保证公平
- 后台低功耗任务用协作式,减少切换开销
比如我之前做的一个工业控制器:
- 电机控制任务:优先级最高,抢占式,每100μs响应一次
- 通信协议栈任务:中等优先级,时间片轮转,每个10ms
- 日志记录任务:最低优先级,协作式,有空就跑
你想想看,如果所有任务都用抢占式,电机控制倒是爽了,但日志任务可能永远跑不了。如果全用协作式,电机控制又可能被日志任务拖死。混合使用才是王道。
我的建议:选调度算法前,先列清楚每个任务的实时性要求、执行时间、周期。然后画个时序图,看看最坏情况下能不能满足。别凭感觉选,要算清楚。
嗯,三种调度算法各有千秋。选哪个,取决于你的项目需求、硬件资源、还有你的胆量。我个人建议:新手先从协作式入手,理解任务切换的本质;然后尝试时间片轮转,感受公平调度;最后再挑战优先级抢占,处理各种竞态问题。
记住一点:没有完美的调度算法,只有适合的调度算法。你想想看,一个跑在8位MCU上的温控器,非要上抢占式RTOS,那不是杀鸡用牛刀吗?
公众号:蓝海资料掘金营,微信deep3321