3. 自旋锁在 RT 模型下的行为变化:自旋锁变为可睡眠锁(rt_mutex)、raw_spinlock 与 spinlock 的区别

在标准 Linux 内核(非实时)中,spinlock 是纯粹的忙等待锁:如果锁被持有,申请者会在一个紧凑循环中反复检查锁状态,期间不释放 CPU。但在 PREEMPT_RT(实时内核抢占模型)下,这一行为发生了根本性变化——spinlock 被内核替换为基于 rt_mutex 的可睡眠锁。这一改动极大降低了实时任务因忙等待导致的优先级反转和调度延迟,但也引入了新的故障模式。

核心变化: 在 RT 内核中,spinlock 不再是自旋锁,而是可睡眠的互斥锁。持有锁的线程可以被更高优先级的任务抢占,而等待锁的线程会进入睡眠状态,不再消耗 CPU 时间。

3.1 自旋锁 → rt_mutex:行为变化全景

下表总结了 spinlock 在标准内核与 RT 内核下的关键行为差异:

特性 标准内核 (PREEMPT_NONE/VOLUNTARY) RT 内核 (PREEMPT_RT)
等待机制 忙等待(自旋循环) 睡眠等待(基于 rt_mutex)
持有期间抢占 禁止抢占(隐式关闭抢占) 允许抢占(持有者可能被更高优先级任务抢占)
中断上下文使用 允许(不睡眠) 禁止(会触发 BUG:睡眠中原子上下文)
优先级继承 无 有(rt_mutex 原生支持优先级继承)
锁持有时间 必须极短(微秒级) 可相对较长(毫秒级,但需谨慎)
底层实现 原子操作 + 内存屏障 rt_mutex + 等待队列 + PI 逻辑
故障排查要点: 在 RT 内核中,如果在中断上下文(hardirq / softirq / 不可睡眠的 kthread)中获取 spinlock,会触发 BUG: sleeping function called from invalid context。这是 RT 迁移中最常见的故障之一。

3.2 raw_spinlock 与 spinlock 的区别

为了在 RT 内核中保留“真正的自旋锁”语义,内核引入了 raw_spinlock。理解两者的区别是 RT 调试的基石:

  • spinlock:在标准内核中是自旋锁;在 RT 内核中被重定义为 rt_mutex,变为可睡眠锁。适用于绝大多数临界区。
  • raw_spinlock:在所有内核配置下都是真正的自旋锁(忙等待)。用于绝对不允许睡眠的极短临界区,如调度器内部、中断控制器操作、底层时间维护。

下表从多个维度对比两者:

维度 raw_spinlock spinlock (RT 下)
RT 下是否睡眠 否(始终自旋) 是(变为 rt_mutex)
中断上下文可用 是 否
持有时间限制 极短(纳秒~微秒) 相对宽松(微秒~毫秒)
优先级继承 无 有
典型使用场景 调度器 rq lock、中断控制器、hwspinlock 设备驱动、文件系统、网络协议栈
API 前缀 raw_spin_lock() spin_lock()
调试口诀: 在 RT 内核中,看到 spin_lock() 要想到“可能睡眠”;看到 raw_spin_lock() 要想到“绝对自旋”。如果某个临界区在中断上下文中被调用,必须使用 raw_spin_lock()。

3.3 架构对比:spinlock 在 RT 下的内部转换

下面的 SVG 图展示了标准内核与 RT 内核中 spinlock 的架构差异,以及 raw_spinlock 的独立地位:

标准内核 vs RT 内核:spinlock 架构对比 标准内核 (PREEMPT_NONE) spin_lock() 原子操作 + 忙等待循环 禁止抢占 持有期间不可被调度 锁持有时间必须极短 否则导致高优先级任务延迟 特点 • 中断上下文可用 • 无优先级继承 • 低延迟但高 CPU 消耗 RT 内核 (PREEMPT_RT) spin_lock() → rt_mutex 可睡眠锁 + 优先级继承 允许抢占 持有者可被更高优先级任务抢占 锁持有时间可相对较长 等待者睡眠,不消耗 CPU 特点 • 中断上下文不可用 • 支持优先级继承 • 降低实时任务延迟抖动 raw_spinlock:两种内核下均为真正的自旋锁,用于绝对不允许睡眠的极短临界区

3.4 故障场景:中断上下文中的 spinlock

在 RT 内核中,以下代码模式会触发致命错误:

/* 错误示例:在中断上下文中使用 spinlock(RT 下会崩溃) */
void my_interrupt_handler(void)
{
    unsigned long flags;
    spin_lock_irqsave(&my_lock, flags);  /* RT 下:尝试睡眠,但当前在中断上下文 */
    /* ... 临界区 ... */
    spin_unlock_irqrestore(&my_lock, flags);
}

正确的做法是使用 raw_spinlock:

/* 正确示例:中断上下文必须使用 raw_spinlock */
raw_spinlock_t my_raw_lock;

void my_interrupt_handler(void)
{
    unsigned long flags;
    raw_spin_lock_irqsave(&my_raw_lock, flags);  /* 始终自旋,不睡眠 */
    /* ... 极短临界区 ... */
    raw_spin_unlock_irqrestore(&my_raw_lock, flags);
}
故障排查提示: 如果在 RT 内核中遇到 BUG: sleeping function called from invalid context,且调用栈指向 spin_lock(),请检查该锁是否在中断上下文或不可睡眠的 kthread 中被获取。解决方案是将该锁替换为 raw_spinlock,或重构代码使其在进程上下文中执行。

3.5 优先级继承:rt_mutex 带来的关键优势

RT 内核将 spinlock 转换为 rt_mutex 后,自动获得了优先级继承能力。下图展示了优先级继承如何解决经典的无界优先级反转问题:

rt_mutex 优先级继承:解决无界优先级反转 时间→ 任务 H (高) 任务 M (中) 任务 L (低) 持有锁 等待锁 优先级继承 继承高优先级 释放锁 获取锁 执行 优先级继承效果 任务 L 持有锁期间临时提升到任务 H 的优先级,防止任务 M 抢占 L,从而避免无界优先级反转 任务 H 的等待时间 = L 持有锁的时间(有界),而非 L + M 的执行时间(无界)

3.6 故障排查清单:spinlock 在 RT 下的常见问题

  1. 中断上下文使用 spinlock → 替换为 raw_spinlock 或重构为进程上下文。
  2. 锁持有时间过长导致调度延迟 → 检查临界区中是否有耗时操作(如 I/O、内存分配),将其移出锁范围。
  3. 优先级继承导致意外优先级提升 → 使用 lockdep 检查锁依赖图,避免死锁。
  4. raw_spinlock 持有时间过长 → raw_spinlock 在 RT 下仍会关闭本地中断,长时间持有会增大中断延迟。
  5. spinlock 与 raw_spinlock 混用 → 确保在同一个临界区中不混合使用两种锁类型,否则可能导致锁顺序反转。
调试工具推荐: 使用 lockdep(内核锁依赖验证器)检测 RT 下的 spinlock 使用问题。在 RT 内核中,lockdep 会特别标记在原子上下文中获取可睡眠锁的行为。内核配置中启用 CONFIG_DEBUG_ATOMIC_SLEEP 可捕获此类错误。