4、SCHED_FIFO 调度策略详解

SCHED_FIFO(First In, First Out)是 Linux 实时调度策略中最基础、最直观的一种。它遵循严格的先进先出规则,一旦一个实时任务获得 CPU,除非它主动让出或被更高优先级的实时任务抢占,否则将一直运行下去。本章将深入剖析其核心机制、抢占行为、优先级继承协议,以及在实际应用中可能遇到的优先级反转和饥饿问题。

核心要点: SCHED_FIFO 的核心是“一次运行到阻塞或主动让出”,没有时间片的概念。它依赖优先级进行调度,优先级数字越小,优先级越高(与常规 nice 值相反)。

4.1 先进先出规则与运行队列

SCHED_FIFO 的调度逻辑非常简洁:系统为每个实时优先级(0-99)维护一个 FIFO 运行队列。当 CPU 需要选择下一个任务时,调度器会从最高优先级的非空队列中,选择队首的任务执行。

  • 入队: 当一个 SCHED_FIFO 任务变为可运行状态(例如,从睡眠中唤醒)时,它被添加到其对应优先级队列的 尾部。
  • 出队: 当任务主动让出 CPU(如调用 sched_yield())或阻塞(如等待 I/O)时,它被移出队列。当它再次变为可运行状态时,重新加入队尾。
  • 运行: 调度器总是选择当前最高优先级非空队列的队首任务来执行。
SCHED_FIFO 多优先级运行队列结构 优先级 99 (最高) 任务 A 任务 B → ... 优先级 50 任务 C 任务 D 任务 E 优先级 1 (最低) 任务 F 调度器选择最高优先级非空队列的队首 说明:调度器总是从最高优先级(99)开始向下扫描,选择第一个非空队列的队首任务执行。
关键区别: 与 SCHED_RR 不同,SCHED_FIFO 任务没有时间片。如果它不主动让出 CPU(例如,通过 sched_yield() 或阻塞),并且没有更高优先级的任务就绪,它将无限期地运行下去。

4.2 抢占行为

SCHED_FIFO 的抢占行为是严格基于优先级的。抢占只发生在以下两种场景:

  1. 高优先级任务就绪: 当一个优先级更高的 SCHED_FIFO 或 SCHED_RR 任务变为可运行状态时,它会立即抢占当前正在运行的低优先级实时任务。被抢占的任务被放回其优先级队列的 队首(因为它没有主动让出 CPU)。
  2. 中断处理完成: 中断处理程序(或下半部)可能会唤醒一个高优先级的实时任务。当中断处理结束后,调度器会检查是否有更高优先级的任务就绪,如果有,则进行抢占。

非实时任务(SCHED_OTHER)永远不会抢占任何实时任务。实时任务之间的抢占完全由优先级数字决定。

SCHED_FIFO 抢占行为时序图 t0 t1 t2 t3 低优先级任务 (Prio 50) 高优先级任务就绪 高优先级任务 (Prio 99) 抢占执行 低优先级任务恢复 抢占行为说明: 1. t0-t1: 低优先级任务运行。 2. t1: 高优先级任务就绪,立即抢占。低优先级任务被放回其队列队首。 3. t1-t2: 高优先级任务运行。t2: 高优先级任务阻塞或主动让出。 4. t2-t3: 低优先级任务从队首恢复执行。

4.3 优先级继承协议

优先级继承协议(Priority Inheritance Protocol, PIP)是解决优先级反转问题的一种有效方法。Linux 内核的实时互斥锁(futex 的实时扩展)实现了 PIP。

核心思想: 当一个低优先级任务持有一个高优先级任务所需的锁时,低优先级任务会 临时继承 高优先级任务的优先级。这可以防止中等优先级任务(不持有锁)抢占低优先级任务,从而加速锁的释放。

  1. 正常情况: 任务 L(低优先级)持有锁,任务 H(高优先级)等待锁。
  2. 继承发生: 任务 L 的优先级被提升到任务 H 的优先级(例如,从 50 提升到 99)。
  3. 防止中间干扰: 任务 M(中等优先级,例如 60)无法再抢占任务 L,因为任务 L 现在的优先级(99)高于任务 M(60)。
  4. 释放与恢复: 任务 L 运行并释放锁,其优先级恢复为原始值(50)。任务 H 获得锁并继续执行。
优先级继承协议 (PIP) 工作流程 任务 H (高优先级) 任务 L (低优先级) 任务 M (中优先级) 共享锁资源 请求锁 持有锁 优先级继承 (Prio 50 → 99) 被阻止抢占 流程说明: 1. 任务 L 持有锁,任务 H 请求锁并阻塞。 2. 任务 L 继承任务 H 的优先级,防止任务 M 抢占。 3. 任务 L 释放锁后,优先级恢复,任务 H 获得锁继续执行。

4.4 典型使用场景

SCHED_FIFO 适用于那些需要 确定性的、低延迟的响应,并且任务执行时间相对较短或可预测的场景。

场景 描述 原因
中断处理线程 将中断处理程序下半部作为 SCHED_FIFO 线程运行。 确保关键硬件事件得到最快速的响应,避免被其他任务延迟。
实时控制循环 机器人、CNC 机床、飞行控制器中的周期性控制任务。 需要严格的执行周期和低抖动,SCHED_FIFO 配合 clock_nanosleep() 可以实现。
音频处理 专业音频应用中的音频数据流处理任务。 防止音频缓冲区欠载或溢出,需要保证处理线程在特定时间内完成。
数据采集 高速数据采集卡的数据读取线程。 确保数据不会因为调度延迟而丢失,需要及时从硬件 FIFO 中读取数据。

4.5 潜在问题:优先级反转与饥饿

尽管 SCHED_FIFO 简单高效,但如果不谨慎使用,会引入两个严重的实时系统问题。

4.5.1 优先级反转

优先级反转是指一个高优先级任务被一个低优先级任务阻塞,而一个中等优先级任务(不持有锁)却可以抢占低优先级任务,导致高优先级任务无限期等待。

经典场景:

  1. 任务 L(低优先级)获得锁。
  2. 任务 H(高优先级)请求同一锁,被阻塞。
  3. 任务 M(中等优先级)就绪,抢占任务 L(因为任务 L 优先级低)。
  4. 任务 M 长时间运行,任务 L 无法释放锁,任务 H 无限期等待。
严重后果: 优先级反转可能导致系统错过关键截止时间,甚至造成灾难性后果。例如,在飞行控制系统中,一个高优先级的控制任务可能因为优先级反转而无法及时执行,导致飞机失稳。

解决方案:

  • 优先级继承协议 (PIP): 如上节所述,Linux 内核已实现。
  • 优先级天花板协议 (PCP): 将持有锁的任务的优先级提升到所有可能请求该锁的任务的最高优先级。
  • 避免共享资源: 在实时任务中尽量减少或避免使用互斥锁。

4.5.2 饥饿

饥饿是指一个或多个低优先级任务因为高优先级任务持续占用 CPU 而永远无法获得执行机会。

在 SCHED_FIFO 中:

  • 如果一个高优先级的 SCHED_FIFO 任务进入一个无限循环(例如,while(1);),并且从不主动让出 CPU,那么所有比它优先级低的任务都将被饿死。
  • 即使高优先级任务不是无限循环,如果它频繁地被唤醒(例如,通过高分辨率定时器),它也会持续抢占低优先级任务,导致低优先级任务进展缓慢或完全停滞。
SCHED_FIFO 饥饿问题示意图 高优先级任务 (Prio 99) 持续运行 (while(1)) 低优先级任务 (Prio 1) 永远无法获得 CPU ✕ 饥饿问题说明: 1. 高优先级任务进入无限循环,从不主动让出 CPU。 2. 所有比它优先级低的任务(包括其他实时任务和普通任务)都无法获得 CPU 时间,导致系统功能异常。

解决方案:

  • 使用 SCHED_RR: 对于需要长时间运行的任务,使用 SCHED_RR 代替 SCHED_FIFO,强制进行时间片轮转。
  • 合理设计任务: 确保高优先级任务不会无限循环,并在适当位置调用 sched_yield() 或阻塞。
  • 使用监控看门狗: 设置一个更高优先级的监控任务,定期检查系统状态,如果发现某个任务运行时间过长,可以采取措施(如降低其优先级或重启)。
  • 优先级隔离: 将关键实时任务绑定到独立的 CPU 核心上,避免与其他任务竞争。
最佳实践: 在设计实时系统时,应遵循“优先级分配原则”:任务越关键,优先级越高。同时,必须通过锁协议(如 PIP)和任务设计来避免优先级反转和饥饿。SCHED_FIFO 适合短小精悍的实时任务,对于长时间运行的计算密集型实时任务,应优先考虑 SCHED_RR 或 SCHED_DEADLINE。