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 的独立地位:
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 后,自动获得了优先级继承能力。下图展示了优先级继承如何解决经典的无界优先级反转问题:
3.6 故障排查清单:spinlock 在 RT 下的常见问题
- 中断上下文使用 spinlock → 替换为
raw_spinlock或重构为进程上下文。 - 锁持有时间过长导致调度延迟 → 检查临界区中是否有耗时操作(如 I/O、内存分配),将其移出锁范围。
- 优先级继承导致意外优先级提升 → 使用
lockdep检查锁依赖图,避免死锁。 - raw_spinlock 持有时间过长 → raw_spinlock 在 RT 下仍会关闭本地中断,长时间持有会增大中断延迟。
- spinlock 与 raw_spinlock 混用 → 确保在同一个临界区中不混合使用两种锁类型,否则可能导致锁顺序反转。
调试工具推荐: 使用
lockdep(内核锁依赖验证器)检测 RT 下的 spinlock 使用问题。在 RT 内核中,lockdep 会特别标记在原子上下文中获取可睡眠锁的行为。内核配置中启用 CONFIG_DEBUG_ATOMIC_SLEEP 可捕获此类错误。