2. PREEMPT_RT 架构:内核抢占模型与实时调度策略

在深入排查 PREEMPT_RT 延迟抖动之前,必须理解 Linux 内核的四种抢占模型以及实时线程的调度策略。抢占模型决定了内核在何种时机可以打断当前执行的任务,而调度策略则决定了实时线程如何获得 CPU 资源。这两者共同构成了实时系统的底层行为框架,也是延迟抖动的根本来源。

2.1 内核抢占模型对比

Linux 内核从通用分时系统演进而来,其抢占能力经历了从无到有、从粗粒度到细粒度的演变。下表对比了四种主流抢占模型的核心差异:

抢占模型 内核配置宏 抢占时机 最大延迟 适用场景
PREEMPT_NONE CONFIG_PREEMPT_NONE 仅在返回用户空间时 高(100ms+) 服务器、批处理
PREEMPT_VOLUNTARY CONFIG_PREEMPT_VOLUNTARY 在显式的抢占点(如调度点、锁释放) 中(10ms~100ms) 桌面、轻度交互
PREEMPT CONFIG_PREEMPT 内核态几乎所有可抢占点(除临界区) 低(1ms~10ms) 嵌入式、低延迟桌面
PREEMPT_RT CONFIG_PREEMPT_RT 几乎全部内核代码(包括中断处理、自旋锁) 极低(us 级) 工业控制、音频、航空航天
关键洞察: PREEMPT_RT 的核心贡献在于将大部分自旋锁替换为可抢占的 rt_mutex,并将中断处理线程化,从而消除了内核中最大的不可抢占区间。

下面通过一张 SVG 架构图直观展示四种模型在内核态与用户态之间的抢占边界差异:

Linux 内核抢占模型对比架构图 Linux 内核抢占模型对比 用户态进程 内核态不可抢占区 内核态可抢占区 PREEMPT_NONE 用户态进程 A(运行中) 内核态(系统调用/中断处理)— 完全不可抢占 用户态进程 B(等待,直到 A 返回用户空间) PREEMPT_VOLUNTARY 用户态进程 A(运行中) 不可抢占区 可抢占区(显式抢占点) PREEMPT 用户态进程 A(运行中) 临界区 可抢占区(除临界区外) PREEMPT_RT 用户态进程 A(运行中) 内核态(几乎全部可抢占,中断线程化)
排查提示: 在非 PREEMPT_RT 内核中,一个长时间运行的系统调用(如 read() 从慢速设备读取大量数据)可以阻塞高优先级实时线程数百毫秒。这是延迟抖动最常见的根源之一。

2.2 实时线程调度策略

PREEMPT_RT 内核提供了三种实时调度策略,它们决定了线程在就绪队列中的排序与执行规则。理解这些策略是分析调度延迟的前提。

2.2.1 SCHED_FIFO(先入先出)

  • 规则: 优先级最高的线程一旦就绪,立即抢占当前运行的低优先级线程。同优先级线程按队列顺序执行,直到主动让出(如睡眠、等待 I/O)或被更高优先级线程抢占。
  • 特点: 无时间片轮转,一个线程可以无限期占用 CPU。
  • 风险: 如果高优先级线程陷入死循环或长时间忙等,低优先级线程(包括关键内核线程)将永久饿死。

2.2.2 SCHED_RR(轮转)

  • 规则: 与 SCHED_FIFO 类似,但同优先级线程之间分配固定时间片(默认 100ms)。时间片耗尽后,线程被放到队列尾部。
  • 特点: 提供了同优先级线程间的公平性,防止单个线程独占 CPU。
  • 适用: 多个同等重要的实时任务需要共享 CPU 的场景。

2.2.3 SCHED_DEADLINE(截止时间)

  • 规则: 基于最早截止时间优先(EDF)算法。每个线程指定三个参数:runtime(每周期最大执行时间)、deadline(相对截止时间)、period(周期)。内核保证在 deadline 之前完成 runtime 的执行。
  • 特点: 可调度性分析最强,适合硬实时任务。
  • 限制: 需要精确的任务时序参数,过度使用可能导致系统过载。
调度策略 优先级范围 时间片 抢占行为 典型延迟抖动
SCHED_FIFO 1~99 无 高优先级立即抢占 低(但存在优先级反转风险)
SCHED_RR 1~99 100ms(默认) 同优先级时间片轮转 中(时间片切换引入抖动)
SCHED_DEADLINE 0(内部动态) 由 runtime/period 决定 基于截止时间 极低(可预测性强)
常见陷阱: 将实时线程设置为 SCHED_FIFO 优先级 99 并绑定到同一个 CPU 核心,如果该线程执行 sched_yield() 或忙等,会导致该核心上的其他所有线程(包括中断处理)被饿死。排查时务必检查 /proc/sched_debug 中的调度统计。

下面通过一张调度流程图展示三种策略在就绪队列中的行为差异:

实时调度策略执行流程图 实时调度策略执行流程 SCHED_FIFO 优先级 80 线程 A 优先级 80 线程 B(等待) 优先级 50 线程 C(饿死) A 不主动让出,B/C 无法运行 SCHED_RR 优先级 80 线程 A(运行中) 优先级 80 线程 B(就绪) 时间片耗尽 → 切换 时间片轮转 SCHED_DEADLINE 任务 D(deadline=10ms) 任务 E(deadline=15ms) 任务 F(deadline=20ms) EDF 调度器始终选择 deadline 最早的任务

2.3 抢占模型与调度策略的协同

抢占模型和调度策略并非独立工作。PREEMPT_RT 内核通过以下机制确保实时线程的确定性:

  1. 中断线程化: 硬件中断被转换为内核线程,运行在 SCHED_FIFO 优先级 50。这意味着实时线程(优先级 80~99)可以抢占中断处理,消除中断带来的不可预测延迟。
  2. 自旋锁替换: 所有自旋锁被替换为 rt_mutex,支持优先级继承,防止优先级反转。
  3. RCU 抢占: 读-复制-更新(RCU)机制在 PREEMPT_RT 下支持抢占,不再阻塞实时线程。
实战要点: 在排查延迟抖动时,首先确认内核是否启用了 CONFIG_PREEMPT_RT。然后检查实时线程的调度策略和优先级。一个常见的错误是使用 SCHED_OTHER(非实时策略)运行关键任务,导致其被内核线程随意抢占。

通过本章的学习,你应该能够:

  • 区分四种抢占模型对延迟的影响范围。
  • 根据任务需求选择合适的实时调度策略。
  • 理解 PREEMPT_RT 如何通过架构改造实现微秒级确定性。