4. PREEMPT_RT 核心机制(一):自旋锁与互斥锁的实时化改造

在标准 Linux 内核中,自旋锁(spinlock)和互斥锁(mutex)是两种最基础的同步原语。然而,在实时性要求极高的场景下,它们的行为会导致不可预测的调度延迟。PREEMPT_RT 补丁集对这两类锁进行了根本性的改造,核心目标是将“忙等待”和“优先级反转”对实时任务的影响降至最低。本章将深入剖析 rt_mutex 与 raw_spinlock 的设计哲学与实现机制。

4.1 标准锁的实时性缺陷

在深入 PREEMPT_RT 的改造之前,必须先理解标准锁在实时系统中的“原罪”。

锁类型 标准行为 实时性缺陷
标准自旋锁 忙等待(spin-wait),持有锁期间禁止抢占 高优先级任务被低优先级任务持有的锁阻塞,且无法抢占锁持有者,导致优先级反转无限期延长。
标准互斥锁 睡眠等待,支持优先级继承(PI) 在中断上下文或不可抢占的临界区中无法使用;PI 实现复杂,且在某些边界条件下仍存在延迟抖动。
读写锁 读者与写者竞争 写者可能被大量读者饿死,导致实时写任务无法获得锁。
核心矛盾: 标准自旋锁的“关抢占”行为直接破坏了 PREEMPT_RT 的“完全可抢占”目标。任何关抢占的临界区都会成为实时性的“黑洞”。

4.2 自旋锁的实时化改造:raw_spinlock 与 rt_spinlock

PREEMPT_RT 对自旋锁的改造策略是:将大部分自旋锁替换为可睡眠的互斥锁。但内核中有些极短、且必须在关中断或关抢占下执行的临界区(如调度器内部、中断控制器操作),不能容忍睡眠。因此,PREEMPT_RT 引入了两种自旋锁变体。

4.2.1 改造策略:从忙等到睡眠

在 PREEMPT_RT 内核中,spinlock_t 不再是一个纯粹的忙等锁。它的底层实现被替换为 rt_mutex(实时互斥锁)。当任务试图获取一个已被持有的 spinlock_t 时,它不会原地自旋,而是进入睡眠状态,让出 CPU。这彻底消除了忙等待带来的 CPU 浪费和不可预测性。

关键理解: 在 PREEMPT_RT 下,spin_lock() 函数内部实际上调用的是 rt_mutex_lock()。这意味着持有自旋锁的任务可以被更高优先级的任务抢占,只要它不访问被锁保护的共享数据。

4.2.2 raw_spinlock:最后的“不可抢占”堡垒

并非所有临界区都能容忍睡眠。例如,在中断处理程序(尤其是 NMI)或调度器内部,任何睡眠操作都会导致系统崩溃。为此,PREEMPT_RT 保留了 raw_spinlock_t,它保持了标准自旋锁的原始语义:忙等待 + 关抢占/关中断。

  • 使用场景: 调度器核心路径(rq->lock)、中断控制器操作、时间子系统、底层硬件访问。
  • 性能特征: 极低的获取延迟(无上下文切换),但会阻塞抢占,因此临界区必须极短(通常 < 10 微秒)。
  • 代码标识: 开发者必须显式使用 raw_spin_lock() 和 raw_spin_unlock()。
开发警示: 在 PREEMPT_RT 内核中,如果你错误地使用了 spin_lock() 来保护一个必须关中断的硬件寄存器访问,系统可能会因为锁的睡眠行为而触发死锁或数据损坏。正确的做法是使用 raw_spin_lock_irqsave()。

4.2.3 自旋锁改造架构图

下图展示了 PREEMPT_RT 如何将标准自旋锁分层改造为可睡眠锁,同时保留 raw_spinlock 作为底层基础。

PREEMPT_RT 自旋锁改造架构 spin_lock() / spin_unlock() rt_mutex_lock() / rt_mutex_unlock() (可睡眠、支持优先级继承) 关抢占/中断路径 raw_spin_lock() 忙等待 + 关抢占 可睡眠路径 调度器 上下文切换 核心原则 spin_lock() 在 RT 下可睡眠;raw_spin_lock() 保留原始忙等语义

4.3 互斥锁的实时化改造:rt_mutex 与优先级继承

标准互斥锁虽然支持睡眠,但其优先级继承(PI)协议在实现上存在局限。PREEMPT_RT 引入了全新的 rt_mutex,它实现了更健壮、更高效的优先级继承协议,并作为所有可睡眠锁(包括改造后的 spinlock_t)的底层基础。

4.3.1 rt_mutex 的核心数据结构

rt_mutex 的结构比标准互斥锁更复杂,因为它需要维护一个等待队列,并支持优先级继承链的追踪。

// 简化后的 rt_mutex 结构(位于 kernel/locking/rtmutex.c)
struct rt_mutex {
    raw_spinlock_t      wait_lock;      // 保护等待队列的自旋锁(raw)
    struct rb_root_cached   waiters;    // 红黑树管理的等待队列(按优先级排序)
    struct task_struct      *owner;     // 当前持有者(低 2 位用于标志)
};
  • wait_lock: 使用 raw_spinlock_t 保护,确保在操作等待队列时不会被抢占或中断打断。
  • waiters: 红黑树结构,等待任务按优先级排序,最高优先级的等待者位于最左侧。
  • owner: 指向当前持有锁的任务。低 2 位被用作标志位(如 RT_MUTEX_HAS_WAITERS),用于指示是否有等待者。

4.3.2 优先级继承(PI)的实时化实现

当高优先级任务(任务 H)被低优先级任务(任务 L)持有的锁阻塞时,rt_mutex 会临时将任务 L 的优先级提升到任务 H 的优先级。这样,任务 L 就能尽快运行并释放锁,从而解除任务 H 的阻塞。

PREEMPT_RT 的 PI 实现具有以下特点:

  1. 链式继承: 如果任务 L 本身也在等待另一个锁,PI 会沿着锁的依赖链向上传播,提升链上所有中间任务的优先级。
  2. 非递归: 一个任务可以多次获取同一个 rt_mutex(可重入),但 PI 只会在第一次获取时生效。
  3. 快速路径: 当没有竞争时(即锁未被持有),获取和释放操作仅需原子操作,无需进入慢速路径。
性能指标: 在无竞争情况下,rt_mutex 的获取/释放延迟约为 20-40 纳秒(取决于 CPU 架构)。在有竞争且触发 PI 时,延迟会增加至微秒级,但这是保证实时性所必须付出的代价。

4.3.3 rt_mutex 优先级继承流程图

下图展示了当三个不同优先级的任务竞争同一把 rt_mutex 时,优先级继承如何动态调整持有者的优先级。

rt_mutex 优先级继承(PI)流程 阶段 1:初始状态 任务 H(优先级 90) 任务 M(优先级 50) 任务 L(优先级 20) 锁(空闲) 获取锁 阶段 2:任务 L 持有锁,任务 H 被阻塞 任务 H(阻塞中) 任务 M(就绪) 任务 L(持有锁) 锁(被持有) 尝试获取锁(阻塞) 阶段 3:优先级继承生效 任务 H(阻塞中) 任务 M(被抢占) 任务 L(优先级提升至 90) 锁(被持有) 优先级继承效果 任务 L 的优先级从 20 提升至 90(与任务 H 相同),从而可以抢占任务 M,尽快释放锁。

4.4 锁类型选择指南

在 PREEMPT_RT 内核中,选择合适的锁类型是保证实时性能的关键。以下表格总结了不同锁类型的使用场景和性能特征。

锁类型 PREEMPT_RT 行为 临界区要求 典型延迟 推荐场景
raw_spinlock_t 忙等待 + 关抢占/中断 极短(< 10 µs),不能睡眠 10-50 ns 调度器、中断控制器、时间子系统、硬件寄存器访问
spinlock_t 基于 rt_mutex,可睡眠 可长可短,允许睡眠 100 ns - 10 µs(无竞争) 大多数设备驱动、文件系统、网络协议栈
rt_mutex 可睡眠,支持 PI 可长可短,允许睡眠 200 ns - 20 µs(无竞争) 用户态实时锁(futex)、内核显式 PI 需求
mutex 基于 rt_mutex 实现 可长可短,允许睡眠 与 rt_mutex 相同 标准内核互斥锁,自动获得 PI 能力
最佳实践: 在编写 PREEMPT_RT 内核代码时,默认使用 spinlock_t。只有当你能明确证明临界区必须在关中断/关抢占下执行时,才使用 raw_spinlock_t。滥用 raw_spinlock_t 会破坏系统的实时性。

4.5 代码示例:从 spinlock 到 raw_spinlock 的迁移

以下示例展示了一个标准内核驱动中的锁使用,以及如何在 PREEMPT_RT 下进行正确迁移。

4.5.1 标准内核代码(非 RT)

// 标准内核:使用 spinlock 保护硬件寄存器访问
static spinlock_t hw_lock;
static void hw_write_reg(unsigned int val) {
    unsigned long flags;
    spin_lock_irqsave(&hw_lock, flags);
    // 直接写入硬件寄存器
    writel(val, REG_BASE + REG_OFFSET);
    spin_unlock_irqrestore(&hw_lock, flags);
}

4.5.2 PREEMPT_RT 下的正确迁移

// PREEMPT_RT:必须使用 raw_spinlock 保护硬件寄存器访问
static raw_spinlock_t hw_lock;
static void hw_write_reg(unsigned int val) {
    unsigned long flags;
    raw_spin_lock_irqsave(&hw_lock, flags);
    // 直接写入硬件寄存器
    writel(val, REG_BASE + REG_OFFSET);
    raw_spin_unlock_irqrestore(&hw_lock, flags);
}
迁移陷阱: 如果在 PREEMPT_RT 下使用 spin_lock_irqsave() 保护硬件寄存器访问,锁内部可能会睡眠,导致中断被长时间关闭(因为 local_irq_save 已经执行),从而引发严重的实时延迟。必须使用 raw_spin_lock_irqsave() 来保证临界区不睡眠。

4.6 本章小结

PREEMPT_RT 通过将自旋锁改造为可睡眠锁,并引入健壮的 rt_mutex 优先级继承机制,从根本上解决了标准锁导致的优先级反转和不可预测延迟问题。开发者需要理解 raw_spinlock_t 与 spinlock_t 的本质区别,并在代码中做出正确的选择。这是构建确定性实时系统的第一块基石。