02 · 锁:spinlock_t / rwlock_t / mutex 全部变成 rt_mutex
结论:RT 内核里
spinlock_t、rwlock_t、local_lock_t、mutex、rw_semaphore底层都是带优先级继承(PI)的rt_mutex;只有raw_spinlock_t和 bit spinlock 还是真正的自旋锁。这消除了「持锁不可抢占」和「无界优先级反转」,代价是每次加解锁多做migrate_disable/enable与 RCU 读区进出,争用时要睡眠/唤醒(而非自旋),并且spin_lock_irqsave()在 RT 上根本不关中断——依赖这一点的驱动代码在 RT 上是错的。
第一层:两颗 CPU 争一把锁
先看结论:RT 把等锁从「烧 CPU 自旋」变成「睡眠 + 提升持锁者」,消灭了持锁不可抢占与无界优先级反转。
非 RT 的 qspinlock 是「关抢占 + 自旋」:等锁的 CPU 100% 忙等,持锁 CPU 上任何高优先级任务都进不来;如果持锁者被中断打断,所有等待者跟着延长。RT 的 rt_spin_lock() 在争用时把等待者挂到红黑树并睡眠,同时把持锁者提升到最高等待者的优先级——持锁者尽快跑完,等待者不浪费 CPU。
第二层:完整加解锁路径
先看结论:无争用时只多两次 cmpxchg 和
migrate_disable/enable;有争用时才进红黑树、PI 链和睡眠。spin_lock_irqsave()不再关中断。
类型替换
- 非 RT:
spinlock_t是raw_spinlock的包装(include/linux/spinlock_types.h:14-29)。 - RT:
typedef struct spinlock { struct rt_mutex_base lock; … } spinlock_t;(include/linux/spinlock_types.h:45-61),而rt_mutex_base由一把raw_spinlock_t wait_lock、等待者红黑树waiters和owner指针组成(include/linux/rtmutex.h:23-27)。 raw_spinlock_t在两种内核里完全相同(include/linux/spinlock_types_raw.h:14-23)。rwlock_t→struct rwbase_rt+atomic_t readers(include/linux/rwlock_types.h:53-63)。mutex→ 直接内嵌rt_mutex_base(include/linux/mutex_types.h:56-67);经典 mutex 实现整体被#ifndef CONFIG_PREEMPT_RT排除(kernel/locking/mutex.c:36),mutex_lock()走rtmutex_api.c:498-531的__rt_mutex_lock()。rw_semaphore→rwbase_rt(include/linux/rwsem.h:135-141;实现kernel/locking/rwsem.c:1402-1516)。QUEUED_RWLOCKS在 RT 下不可选(kernel/Kconfig.locks:254),DEBUG_MUTEXES/DEBUG_RWSEMS同样(lib/Kconfig.debug:1456,1481),换成DEBUG_RT_MUTEXES。
RT 的 spin_lock() 做了什么
kernel/locking/spinlock_rt.c:46-52:
static __always_inline void __rt_spin_lock(spinlock_t *lock)
{
rtlock_might_resched(); /* 注解:这把锁可能睡眠 */
rtlock_lock(&lock->lock); /* cmpxchg(owner: NULL→current),失败走 rtlock_slowlock() */
rcu_read_lock(); /* 保持非 RT 下「持自旋锁隐含 RCU 读区」的语义 */
migrate_disable(); /* 替代 preempt_disable:钉住 CPU 但可抢占 */
}
文件头(:3-19)明确写着:非 RT 自旋锁关抢占并最终关中断;RT 替代品显式禁止迁移、并在持锁期间进入 RCU 读区。
spin_lock_irqsave() 不关中断:include/linux/spinlock_rt.h:96-101 就是 flags = 0; spin_lock(lock);,spin_unlock_irqrestore() 忽略 flags(:119-123)。_bh 变体仍会 local_bh_disable()(:84-89),但那在 RT 上也是一把 per-CPU 锁(第 04 章)。spin_is_contended() 恒为 0(:147)。
慢路径:入树、PI、自旋还是睡眠
kernel/locking/rtmutex.c:1838-1904 rtlock_slowlock_locked():
- 持
wait_lock再试一次try_to_take_rt_mutex()(:1846-1847)。 current_save_and_set_rtlock_wait_state()(:1852):把任务状态保存后设为TASK_RTLOCK_WAIT,因此在TASK_INTERRUPTIBLE里拿自旋锁不会丢唤醒(Documentation/locking/locktypes.rst:257-293描述了saved_state协议)。task_blocks_on_rt_mutex()(:1856,定义:1195-1304):按优先级插入红黑树;若成为最高等待者,rt_mutex_adjust_prio(lock, owner)提升持锁者(:1264-1271),持锁者自己也被阻塞时沿链继续rt_mutex_adjust_prio_chain()(:677-723)。- 自旋还是睡眠(
:1873-1874):if (!owner || !rtmutex_spin_on_owner(lock, &waiter, owner)) schedule_rtlock();——只有最高等待者、且持锁者正在别的 CPU 上运行时才自旋(:1486-1521,停止条件:owner 被调度出去、自己不再是顶端等待者、need_resched()、vCPU 被抢占);UP 上永远直接睡(:1522-1529)。 - 释放:
rt_spin_unlock()(spinlock_rt.c:78-104)先migrate_enable(),快路径cmpxchg(owner: current→NULL),有等待者则rt_mutex_slowunlock()唤醒树最左节点并 deboost,最后rcu_read_unlock()。
等待者排序(:393-409):先按 prio,两者都是 SCHED_DEADLINE 时按 deadline 早者优先。PI 链深度上限 max_lock_depth = 1024(kernel/locking/rtmutex_api.c:14,/proc/sys/kernel/max_lock_depth 可调,超限返回 -EDEADLK,rtmutex.c:700-718)。同优先级「横向偷锁」只允许非 RT 优先级的任务(:435-446),避免给 RT 任务引入无界延迟。
rwlock / rwsem:读者偏向,写者不公平
kernel/locking/rwbase_rt.c:4-42:读者走原子计数快路径(READER_BIAS = 1<<31),写者先拿 rtmutex、再减去 READER_BIAS 把读者赶进慢路径、等所有读者离开。RT 的 rwsem/rwlock 不是写者公平的——写者可能饥饿,但写者受 PI 保护;之所以不做多读者继承,是因为 SCHED_DEADLINE 无法支持(:33-39)。locktypes.rst:313-315 直言:一个被抢占的低优先级读者会让高优先级写者饿死。
嵌套规则(RT 特有)
Documentation/locking/locktypes.rst:524-531:RT 把 spinlock_t/rwlock_t/local_lock 从「自旋」类降到「睡眠」类,因此它们不能在 raw_spinlock_t 临界区内获取;嵌套顺序只能是 睡眠锁 → spinlock_t/rwlock_t/local_lock → raw_spinlock_t/bit spinlock。反过来(raw 在 spinlock_t 里)允许。PROVE_RAW_LOCK_NESTING(lib/Kconfig.debug:1398)可以在非 RT 内核上提前检查。
一个直接推论(locktypes.rst:470-489):持 raw_spinlock_t 时不能 kmalloc()(RT 的分配器会睡),但持普通 spinlock_t 时可以。
ARM64 注
- 非 RT 的 arm64 用队列自旋锁/读写锁(
arch/arm64/Kconfig:89-90select ARCH_USE_QUEUED_SPINLOCKS / ARCH_USE_QUEUED_RWLOCKS);RT 下QUEUED_RWLOCKS被kernel/Kconfig.locks:254砍掉,换成rwbase_rt。 - rt_mutex 快路径的
cmpxchg(owner)、READER_BIAS原子加减都受益于 LSE:arch/arm64/Kconfig:1935-1943ARM64_USE_LSE_ATOMICS default y,ARMv8.1+ 用 CAS/LDADD 指令代替 LL/SC 循环,争用下延迟更稳;ARMv8.0(Cortex-A53/A72 等)只能 LL/SC,无争用开销略高、争用时重试次数不确定。 arch/arm64/Kconfig:58-83ARCH_INLINE_*_LOCK if !PREEMPTION:RT 内核里所有 spin/rw 锁操作都是 out-of-line 调用(非抢占 arm64 内核才内联)。raw_spinlock_t临界区在 arm64 上关中断的方式取决于伪 NMI:默认msr daifset,开启irqchip.gicv3_pseudo_nmi=1后写ICC_PMR_EL1(第 03 章)。
性能影响
净影响:无争用锁开销约 2–4 倍,争用锁从自旋变睡眠;这是 RT 吞吐下降的第二大来源,也是延迟有界的基础。
| 场景 | 非 RT qspinlock | RT rt_mutex | 备注 |
|---|---|---|---|
| 无争用 lock+unlock | ~1 次原子操作 + preempt 计数(~20 cycles) | cmpxchg ×2 + migrate_disable/enable + RCU 进出(约 2–4 倍) |
热路径锁(网络、块层)密度高时可见 |
| 争用(持锁者在跑) | 自旋,等待时间 = 剩余临界区 | 自适应自旋(仅顶端等待者) | 相近 |
| 争用(持锁者被抢占/睡眠) | 自旋到持锁者被再次调度(可能很久) | 睡眠 + PI 提升持锁者 | RT 显著更优,这是 PI 的价值 |
| 唤醒成本 | 无 | 2 次上下文切换(几 µs) | 高争用锁吞吐下降 |
spin_lock_irqsave |
关中断 | 不关中断 | 依赖关中断保护的代码必须改 raw 或 local_lock_irq |
| rwlock 读多写少 | 公平 | 读者偏向;写者可能饿 | 写延迟不确定性 |
spin_is_contended() |
有效 | 恒 0 | 依赖它做 yield 的代码退化 |
源码定位
| 主题 | 位置 |
|---|---|
RT spinlock_t 定义 |
include/linux/spinlock_types.h:45-61;include/linux/rtmutex.h:23-27 |
RT spin_lock*() 映射 |
include/linux/spinlock.h:454-455 → include/linux/spinlock_rt.h:42-45, 84-101, 119-123, 147 |
rt_spin_lock/unlock/trylock |
kernel/locking/spinlock_rt.c:38-137 |
| 慢路径与自适应自旋 | kernel/locking/rtmutex.c:1828-1904, 1486-1529 |
| PI 链遍历 / 优先级调整 | kernel/locking/rtmutex.c:526-539, 677-723, 1195-1304;kernel/sched/core.c:7123 |
max_lock_depth |
kernel/locking/rtmutex_api.c:14;kernel/sysctl.c:1937-1945 |
| 等待者排序 | kernel/locking/rtmutex.c:388-427 |
RT_MUTEX_HAS_WAITERS |
kernel/locking/rtmutex_common.h:156;rtmutex.c:67-103, 194-208 |
| rwbase_rt | include/linux/rwbase_rt.h:8-37;kernel/locking/rwbase_rt.c:4-300 |
| RT rwsem / rwlock 粘合 | kernel/locking/rwsem.c:1402-1516;kernel/locking/spinlock_rt.c:164-260 |
| RT mutex | include/linux/mutex_types.h:56-67;include/linux/mutex.h:100-123;kernel/locking/rtmutex_api.c:498-531 |
| 锁类型文档 | Documentation/locking/locktypes.rst:46-50, 229-255, 305-318, 470-534 |
调优要点
- 驱动移植 RT 的三件事:① 依赖
spin_lock_irqsave关中断的地方改raw_spinlock_t或local_lock_irq;② 持 raw 锁时不分配内存;③ 在非 RT 内核上先开PROVE_RAW_LOCK_NESTING跑一遍。 - 争用严重的热点锁在 RT 上退化最明显:用
perf lock contention或lockstat找出来,考虑 per-CPU 化或 RCU 化。 max_lock_depth一般不用动;若日志出现 "Maximum lock depth 1024 reached",说明锁链设计有问题而不是参数太小。
小结
spinlock_t/rwlock_t/local_lock_t/mutex/rwsem底层统一为 rt_mutex,只有raw_spinlock_t与 bit spinlock 还自旋。- PI 链最深 1024、等待者按 prio/deadline 排序、自适应自旋只给顶端等待者——这些细节决定了争用时的行为。
- RT 的 rwlock/rwsem 读者偏向、写者可能饥饿;
spin_lock_irqsave不关中断是驱动移植的头号坑。 - 承上启下:锁可以睡了,hardirq 里却不能睡——所以驱动逻辑必须离开 hardirq,这就是下一章的中断线程化。