4. SCHED_FIFO调度策略详解
SCHED_FIFO(First In, First Out)是 Linux 实时调度策略中最基础、最直观的一种。它遵循严格的先进先出规则,一旦一个实时任务获得 CPU,除非它主动让出或被更高优先级的实时任务抢占,否则将一直运行下去。本章将深入剖析其核心机制、抢占行为、优先级继承协议,以及在实际应用中可能遇到的优先级反转和饥饿问题。
4.1 先进先出规则与运行队列
SCHED_FIFO 的调度逻辑非常简洁:系统为每个实时优先级(0-99)维护一个 FIFO 运行队列。当 CPU 需要选择下一个任务时,调度器会从最高优先级的非空队列中,选择队首的任务执行。
- 入队: 当一个 SCHED_FIFO 任务变为可运行状态(例如,从睡眠中唤醒)时,它被添加到其对应优先级队列的 尾部。
- 出队: 当任务主动让出 CPU(如调用
sched_yield())或阻塞(如等待 I/O)时,它被移出队列。当它再次变为可运行状态时,重新加入队尾。 - 运行: 调度器总是选择当前最高优先级非空队列的队首任务来执行。
sched_yield() 或阻塞),并且没有更高优先级的任务就绪,它将无限期地运行下去。
4.2 抢占行为
SCHED_FIFO 的抢占行为是严格基于优先级的。抢占只发生在以下两种场景:
- 高优先级任务就绪: 当一个优先级更高的 SCHED_FIFO 或 SCHED_RR 任务变为可运行状态时,它会立即抢占当前正在运行的低优先级实时任务。被抢占的任务被放回其优先级队列的 队首(因为它没有主动让出 CPU)。
- 中断处理完成: 中断处理程序(或下半部)可能会唤醒一个高优先级的实时任务。当中断处理结束后,调度器会检查是否有更高优先级的任务就绪,如果有,则进行抢占。
非实时任务(SCHED_OTHER)永远不会抢占任何实时任务。实时任务之间的抢占完全由优先级数字决定。
4.3 优先级继承协议
优先级继承协议(Priority Inheritance Protocol, PIP)是解决优先级反转问题的一种有效方法。Linux 内核的实时互斥锁(futex 的实时扩展)实现了 PIP。
核心思想: 当一个低优先级任务持有一个高优先级任务所需的锁时,低优先级任务会 临时继承 高优先级任务的优先级。这可以防止中等优先级任务(不持有锁)抢占低优先级任务,从而加速锁的释放。
- 正常情况: 任务 L(低优先级)持有锁,任务 H(高优先级)等待锁。
- 继承发生: 任务 L 的优先级被提升到任务 H 的优先级(例如,从 50 提升到 99)。
- 防止中间干扰: 任务 M(中等优先级,例如 60)无法再抢占任务 L,因为任务 L 现在的优先级(99)高于任务 M(60)。
- 释放与恢复: 任务 L 运行并释放锁,其优先级恢复为原始值(50)。任务 H 获得锁并继续执行。
4.4 典型使用场景
SCHED_FIFO 适用于那些需要 确定性的、低延迟的响应,并且任务执行时间相对较短或可预测的场景。
| 场景 | 描述 | 原因 |
|---|---|---|
| 中断处理线程 | 将中断处理程序下半部作为 SCHED_FIFO 线程运行。 | 确保关键硬件事件得到最快速的响应,避免被其他任务延迟。 |
| 实时控制循环 | 机器人、CNC 机床、飞行控制器中的周期性控制任务。 | 需要严格的执行周期和低抖动,SCHED_FIFO 配合 clock_nanosleep() 可以实现。 |
| 音频处理 | 专业音频应用中的音频数据流处理任务。 | 防止音频缓冲区欠载或溢出,需要保证处理线程在特定时间内完成。 |
| 数据采集 | 高速数据采集卡的数据读取线程。 | 确保数据不会因为调度延迟而丢失,需要及时从硬件 FIFO 中读取数据。 |
4.5 潜在问题:优先级反转与饥饿
尽管 SCHED_FIFO 简单高效,但如果不谨慎使用,会引入两个严重的实时系统问题。
4.5.1 优先级反转
优先级反转是指一个高优先级任务被一个低优先级任务阻塞,而一个中等优先级任务(不持有锁)却可以抢占低优先级任务,导致高优先级任务无限期等待。
经典场景:
- 任务 L(低优先级)获得锁。
- 任务 H(高优先级)请求同一锁,被阻塞。
- 任务 M(中等优先级)就绪,抢占任务 L(因为任务 L 优先级低)。
- 任务 M 长时间运行,任务 L 无法释放锁,任务 H 无限期等待。
解决方案:
- 优先级继承协议 (PIP): 如上节所述,Linux 内核已实现。
- 优先级天花板协议 (PCP): 将持有锁的任务的优先级提升到所有可能请求该锁的任务的最高优先级。
- 避免共享资源: 在实时任务中尽量减少或避免使用互斥锁。
4.5.2 饥饿
饥饿是指一个或多个低优先级任务因为高优先级任务持续占用 CPU 而永远无法获得执行机会。
在 SCHED_FIFO 中:
- 如果一个高优先级的 SCHED_FIFO 任务进入一个无限循环(例如,
while(1);),并且从不主动让出 CPU,那么所有比它优先级低的任务都将被饿死。 - 即使高优先级任务不是无限循环,如果它频繁地被唤醒(例如,通过高分辨率定时器),它也会持续抢占低优先级任务,导致低优先级任务进展缓慢或完全停滞。
解决方案:
- 使用 SCHED_RR: 对于需要长时间运行的任务,使用 SCHED_RR 代替 SCHED_FIFO,强制进行时间片轮转。
- 合理设计任务: 确保高优先级任务不会无限循环,并在适当位置调用
sched_yield()或阻塞。 - 使用监控看门狗: 设置一个更高优先级的监控任务,定期检查系统状态,如果发现某个任务运行时间过长,可以采取措施(如降低其优先级或重启)。
- 优先级隔离: 将关键实时任务绑定到独立的 CPU 核心上,避免与其他任务竞争。