spinlock 是内核最基本的同步原语——持有时不能睡眠、不能被抢占。从最简单的 test-and-set 到如今的 qspinlock,四代演进都在解决一个问题:公平性和 NUMA 扩展性。
一、先看图
flowchart LR
TAS[test-and-set<br/>不公平] --> TICKET[ticket lock<br/>FIFO 公平]
TICKET --> MCS[MCS lock<br/>本地自旋]
MCS --> QSPIN[qspinlock<br/>混合方案<br/>4 字节]
QSPIN --> PVQSPIN[paravirt qspinlock<br/>虚拟化优化]
classDef gen fill:#388bfd22,stroke:#388bfd,color:#adbac7;
class TAS,TICKET,MCS,QSPIN,PVQSPIN gen
二、原始 spinlock(test-and-set)
while (test_and_set(&lock))
cpu_relax(); // 自旋问题:
- 不公平:多核竞争时,物理距离近的核更容易抢到
- cache line 风暴:所有核对同一 cacheline 做原子操作
三、ticket lock
struct ticket_lock {
atomic_t next; // 取号
atomic_t owner; // 当前持有者
};每个线程取号 → 等自己号码 = owner → 公平(FIFO)。
问题:所有核自旋在同一 cacheline(owner)→ 释放锁时 invalidate 风暴。
四、MCS lock
struct mcs_node {
struct mcs_node *next;
int locked; // 本地自旋变量
};每个核在自己的 mcs_node.locked 上自旋(本地 cacheline)→ 释放锁时只唤醒下一个。
优势:O(1) cache invalidation → NUMA 友好。 问题:需要额外内存(每核一个 node)→ 不能直接替换 4 字节 spinlock。
五、qspinlock
Linux 4.2+ 的默认 spinlock。4 字节内编码三层状态:
union {
atomic_t val;
struct {
u8 locked; // 是否已锁
u8 pending; // 是否有人等待(fast path)
u16 tail; // MCS 队列尾
};
};5.1 三路径
- Fast path:lock 为 0 → CAS 获取(一条原子指令)
- Pending path:lock 被持有但无队列 → 设 pending 位 → 自旋等 locked 清零
- Slow path:已有 pending → 入 MCS 队列 → 本地自旋
5.2 优势
- 无竞争:和 test-and-set 一样快
- 中等竞争:pending 避免建队列
- 高竞争:MCS 队列避免 cacheline 风暴
六、paravirt qspinlock
虚拟化场景:持锁 vCPU 被 hypervisor 调度走 → 其他 vCPU 白白自旋(lock holder preemption)。
PV qspinlock(4.8+):检测到持锁者未运行 → halt 当前 vCPU → hypervisor 优先调度持锁者。
七、spin_lock 变体
| API | 禁止 | 场景 |
|---|---|---|
spin_lock() |
抢占 | 进程上下文,无中断竞争 |
spin_lock_bh() |
抢占 + 软中断 | 与 softirq 共享数据 |
spin_lock_irq() |
抢占 + 硬中断 | 与硬中断共享数据 |
spin_lock_irqsave() |
同上 + 保存 flags | 不知道当前中断状态 |
错误使用 → 死锁或中断延迟。
八、rwlock 的退休
rwlock_t:多读者并行、写者独占。
问题:读者侧仍是全局计数器 → cache line bouncing → 多核性能差。
替代:RCU(读侧零开销)、percpu_rwsem。
九、PREEMPT_RT 下的 spinlock
PREEMPT_RT 把 spinlock_t 替换为
rt_mutex → 可睡眠 → 优先级继承。
只有 raw_spinlock_t
保持真正的自旋(硬中断路径等)。
十、小结
- test-and-set → ticket → MCS → qspinlock:公平性和扩展性不断改进
- qspinlock 是 Linux 默认,4 字节内编码三路径
- paravirt 解决虚拟化下的 lock holder preemption
- PREEMPT_RT 把 spinlock 变成可睡眠锁
参考文献
kernel/locking/qspinlock.cDocumentation/locking/- Waiman Long, “qspinlock: a 4-byte queue spinlock.” 2014
include/linux/spinlock_types.h
工具
lockstat(/proc/lock_stat)perf lockbpftrace -e 'kprobe:queued_spin_lock_slowpath { @[kstack] = count(); }'
上一篇:原子 RMW 下一篇:mutex/rwsem
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】NUMA 内存
多路服务器里内存不再平等——本地访问和远程访问延迟差 2-3 倍。本文讲 NUMA 拓扑、mempolicy、AutoNUMA、NUMA balancing、CXL 带来的分级内存层次,以及 NUMA 感知的生产调优。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。