2、任务调度策略:优先级抢占调度、时间片轮转、 Deadline调度、调度点与上下文切换开销分析
任务调度,说白了就是操作系统决定「谁先跑、跑多久」的规则。很多刚入行的朋友觉得调度就是调个优先级,其实远没那么简单。我这些年调过的系统里,至少有一半的实时性问题,根子都出在调度策略选型或配置上。
咱们先看一张总览图,把四种核心调度策略的关系理清楚。
2.1 优先级抢占调度
这是最常用的实时调度策略。每个任务有一个优先级,就绪队列里谁优先级高谁先跑。高优先级任务一旦就绪,立刻抢占正在运行的低优先级任务。
我习惯把这种策略比作「急诊插队」——ICU 来的病人必须优先处理。在 FreeRTOS 或 μC/OS-III 里,这就是默认行为。
但这里有个大坑——优先级反转。我曾经在一个电机控制项目里遇到过:低优先级任务持有了一个互斥锁,中优先级任务一直在跑,高优先级任务等锁等到超时,电机直接失控。解决方案是优先级继承协议,RTOS 一般都有这个选项,记得打开。
2.2 时间片轮转
时间片轮转,说白了就是「每人轮流用 CPU,一次用一小会儿」。每个任务分配一个固定长度的时间片,时间到了就切换到下一个就绪任务。
这种策略的好处是公平——不会出现某个任务永远得不到 CPU 的情况。但坏处也很明显:实时性无法保证。你想想看,一个紧急任务可能要等完一圈时间片才能轮到。
| 对比项 | 优先级抢占 | 时间片轮转 |
|---|---|---|
| 实时性 | 强(高优先级可立即响应) | 弱(必须等时间片用完) |
| 公平性 | 差(低优先级可能饿死) | 好(每个任务都有机会) |
| 适用场景 | 硬实时、工业控制 | 软实时、人机交互 |
| 典型 RTOS | FreeRTOS、VxWorks | Linux CFS、RT-Thread |
实际项目中,我经常把两种策略混用。比如:高优先级任务用抢占模式,保证关键响应;同优先级的多个任务用时间片轮转,防止某个任务霸占 CPU。
2.3 Deadline 调度
Deadline 调度是按任务的截止时间来排队的——谁最快要到截止时间,谁先跑。最经典的算法是 EDF(Earliest Deadline First)。
我记得第一次用 EDF 是在一个视频编码器项目里。每帧必须在 33ms 内编码完成,否则画面会卡顿。用优先级抢占死活调不好,换成 EDF 后,CPU 利用率从 65% 提到了 92%,而且没有丢帧。
EDF 的调度决策很简单:每次调度时,选 deadline 最近的任务。但实现上需要维护一个有序队列,插入和删除都是 O(log n) 的复杂度。在任务数少于 20 个的系统中,直接用数组遍历也够用。
2.4 调度点与上下文切换开销
调度点就是操作系统检查「要不要切换任务」的时刻。常见的调度点有:
- 任务主动挂起:调用 delay、pend 等阻塞 API 时
- 时间中断:系统 tick 中断触发,检查时间片是否用完
- 任务创建/删除:任务状态变化时
- 中断退出:中断返回前检查是否有更高优先级任务就绪
上下文切换开销,就是保存当前任务现场、恢复新任务现场所花的时间。这个开销是纯浪费的——CPU 没干任何有用的活。
FreeRTOS 一次上下文切换 ≈ 0.6μs ~ 1.2μs
μC/OS-III 一次上下文切换 ≈ 0.8μs ~ 1.5μs
Linux RT-Preempt 一次上下文切换 ≈ 3μs ~ 8μs
我曾经在一个 1000Hz 控制循环的系统里,把调度频率从每 1ms 一次降到每 5ms 一次,CPU 利用率直接降了 12%。你想想看,如果每秒切换 1000 次,每次浪费 1μs,那就是 1ms 的纯开销——占 CPU 的 0.1%,看起来不多,但在硬实时系统里,这 0.1% 可能就是压垮骆驼的最后一根稻草。
1. 减少不必要的调度点——能用信号量就别用任务切换
2. 合并短任务——把多个 100μs 的小任务合并成一个 500μs 的任务
3. 关中断临界区要短——我见过有人关中断 50μs,这期间所有中断都丢了
4. 使用 FPU 时注意上下文保存——FPU 寄存器有 32 个,保存恢复开销很大
嗯,调度策略这块就聊到这儿。记住一句话:没有最好的调度策略,只有最适合你场景的策略。选型时把任务的时间特性、紧急程度、CPU 负载都列出来,再决定用哪种。