日期 :2026-07-27 · 参考源码版本:Linux 内核 5.x
版权声明 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用。
写在前面
spinlock第一部分讲到 spinlock 的调用链止于 arch_spin_lock---"架构相关的原子获取"。这句话把真正的魔法藏了起来:arch_spin_lock 到底怎么自旋?怎么保证公平?怎么避免总线风暴?
本文揭开这一层,以ARM(32位)为例,讲清 票号锁(ticket lock) 的实现:每个 CPU 取一个号,按号顺序服务。我们会看到 ldrex/strex 独占访问、wfe 低功耗等待、dsb_sev 唤醒,以及内存屏障在获取/释放点的精巧位置。
阅读本文你将了解:
- 简单 test-and-set 自旋锁为何不公平、会引发总线风暴
- 票号锁的 next/owner 排队模型如何保证 FIFO 公平
- ARM
ldrex/strex独占对如何实现原子"取号"wfe/dsb_sev如何降低等待功耗并唤醒smp_mb为何在获取末尾与释放开头各放一次
一、问题:为什么需要票号锁
1.1 简单自旋锁的问题
最朴素的自旋锁是 test-and-set:每个 CPU 在一个共享变量上原子地"读-改-写",抢到 0 者获得锁,其余继续自旋重试。这在多核上有两个问题:
- 不公平:锁释放时所有等待者同时竞争,"运气好"的 CPU 可能反复抢到,某些 CPU 长期饥饿。
- 总线风暴:每次原子读-改-写都会在总线上广播(独占缓存行),N 个等待者每轮触发 N 次总线流量,可扩展性差。
1.2 票号锁思想
票号锁模拟生活中的排队取号:
- next :发号机,每次取号
next+1。 - owner :服务窗口,当前正在服务的号,释放时
owner+1。 - 每个 CPU 取到自己的号后,只等自己的号 == owner。
这样等待者按取号顺序获得锁(FIFO 公平),且释放时只有一个 CPU 的 owner 读取会命中--它发现 owner == 我的号 即获得锁,其余继续 wfe 睡眠。总线流量从 O(N) 降到 O(1)。
二、核心设计思想:next/owner 排队模型
票号锁的核心是把"竞争获取"变成"排队等待"。如图 1 所示,每个 CPU 取号后排在队尾,服务窗口按号推进。
图 1:票号锁排队模型
- 公平:取号顺序即获得锁顺序,先到先得,无饥饿。
- 总线友好 :释放时只
owner+1(一次缓存行写入);等待者用wfe进入低功耗,被sev唤醒后再读owner--只有"下一个该获得锁的 CPU"会真正推进,其余继续睡。 - 类型无关:next/owner 是纯整数计数器,与锁的具体位布局解耦。
不变量:任意时刻
owner <= next;next - owner即当前排队等待者数量;owner == next表示锁空闲。
三、数据结构:arch_spinlock_t
arch/arm/include/asm/spinlock_types.h
c
typedef struct {
union {
u32 slock; // 32 位锁值,next 与 owner 打包
struct __raw_tickets {
#ifdef __ARMEB__ // 大端:先声明字段位于高位
u16 next; // 下一个票号,高 16 位
u16 owner; // 当前服务票号,低 16 位
#else // 小端:字段顺序对调,保 next 恒在高位
u16 owner; // 当前服务票号,低 16 位
u16 next; // 下一个票号,高 16 位
#endif
} tickets;
};
} arch_spinlock_t;
一个 32 位字拆成两半:高 16 位 next、低 16 位 owner(小端情形)。两者打包进一个 u32 是为了用一次原子访问同时读改两者 --ARM 的 ldrex/strex 操作 32 位字,一次独占加载拿到 next+owner,一次独占存储写回更新后的值。大端/小端只是字段顺序调整,语义一致。
TICKET_SHIFT为 16:1 << TICKET_SHIFT即把 next 字段(高 16 位)加 1;owner 字段(低 16 位)加 1 用普通+1。
四、关键操作原理
4.1 arch_spin_lock:独占取号 + 等待
arch/arm/include/asm/spinlock.h
c
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock); // 预取锁到缓存(准备写入)
__asm__ __volatile__(
"1: ldrex %0, [%3]\n" // 独占加载当前锁值到 lockval
" add %1, %0, %4\n" // newval = lockval + (1<<16) 即 next+1
" strex %2, %1, [%3]\n" // 独占存储 newval,%2=0 表示成功
" teq %2, #0\n" // 测试 strex 结果 %2 是否为 0
" bne 1b" // 失败(独占被打破)则重试
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
/* 等待直到自己的票号被服务 */
while (lockval.tickets.next != lockval.tickets.owner) {
wfe(); // 等待事件,降低功耗
lockval.tickets.owner = READ_ONCE(lock->tickets.owner);
}
smp_mb(); // 获取内存屏障
}
分两阶段:
① 取号(ldrex/strex 独占对):
ldrex独占加载slock(拿到当前 next/owner),并对该缓存行打开"独占监视"。add把 next 字段加 1(1 << TICKET_SHIFT),得到newval。strex尝试独占存储newval;若期间有其他 CPU 写过该缓存行,strex失败(返回非 0),bne 1b跳回重试。- 成功后
lockval.tickets.next就是我取到的号(即原 next 值,我的号;锁的 next 已 +1 留给下一位)。
ldrex/strex是 ARM 的独占监视(exclusive monitor)机制:ldrex打开监视,strex仅在监视未被清除时才写入并返回成功。它不锁总线、只监视缓存行,多核扩展性优于早期的swp指令。
② 等待服务(wfe 轮询):
- 若
next != owner(前面还有人),进入while循环:wfe让 CPU 进入低功耗等待,被事件唤醒后重读owner。 READ_ONCE保证每次都从内存读owner(防编译器把它缓存到寄存器)。- 当
owner追上自己的next(即next == owner),获得锁,退出循环。
③ smp_mb():获取锁后加内存屏障,保证后续临界区读写在"已获锁"之后被其他 CPU 观察。持锁流程如图 2 所示。
图 2:arch_spin_lock 持锁流程
4.2 arch_spin_unlock:推进 owner + 唤醒
arch/arm/include/asm/spinlock.h
c
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb(); // 释放内存屏障
lock->tickets.owner++; // owner+1,服务下一个等待者
dsb_sev(); // 数据屏障 + 发送事件唤醒等待者
}
smp_mb():释放前 加屏障,保证临界区内的写操作在"锁释放"之前对其他 CPU 可见--与获取末尾的smp_mb配对,构成完整的临界区内存可见性。owner++:推进服务号,下一个等待者(next == 新 owner)将获得锁。dsb_sev():dsb保证owner++落地后再sev(发送事件),唤醒所有wfe睡眠的 CPU。被唤醒者重读owner,只有"下一个该获锁的"继续推进,其余重新wfe。
解锁流程如图 3 所示。
图 3:arch_spin_unlock 解锁流程
获取把
smp_mb放在末尾、释放放在开头,是经典的 ACQUIRE/RELEASE 语义:获取后临界区不会重排到锁外,释放前临界区不会重排到锁外。两者配对,保证临界区内存操作对下一个持锁者可见。
4.3 arch_spin_trylock:无竞争快速路径
arch/arm/include/asm/spinlock.h
c
static inline int arch_spin_trylock(arch_spinlock_t *lock)
{
unsigned long contended, res;
u32 slock;
prefetchw(&lock->slock);
do {
__asm__ __volatile__(
" ldrex %0, [%3]\n" // 独占加载 slock
" mov %2, #0\n" // 初始化 res=0,供 strexeq 写入结果
" subs %1, %0, %0, ror #16\n" // contended = slock - ROR(slock,16),为 0 则空闲
" addeq %0, %0, %4\n" // 无竞争则 next+1
" strexeq %2, %0, [%3]" // 无竞争才尝试存储
: "=&r" (slock), "=&r" (contended), "=&r" (res)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
} while (res); // strexeq 失败(res≠0)则重试
if (!contended) {
smp_mb();
return 1; // 成功
} else {
return 0; // 已被持有
}
}
trylock 的关键在不自旋 :用 subs %1, %0, %0, ror #16 一条指令判断锁是否空闲。
ROR(slock, 16)把 slock 的高低 16 位对调。若next == owner(锁空闲),则slock == ROR(slock,16),subs结果为 0(contended=0,Z 位置位)。addeq/strexeq仅在 Z 位置位(无竞争)时执行 next+1 并存储--一次原子地"判定空闲 + 取号"。- 有竞争(
contended != 0)则直接返回 0,不自旋。
ROR(slock,16)巧妙地把"比较 next 与 owner 是否相等"压成一条减法:两者相等当且仅当高低 16 位互换后值不变。这是把数据结构布局(next/owner 同宽打包)与指令能力对齐的设计。
4.4 多 CPU 竞争流程
多个 CPU 同时取号时,ldrex/strex 的独占监视保证每次只有一个 CPU 的 strex 成功,失败者重试,从而 next 严格递增无遗漏。竞争流程如图 4 所示。
图 4:多 CPU 竞争取号流程
4.5 操作语义对照表
| 操作 | 语义 | 时间复杂度 | 典型用途 |
|---|---|---|---|
arch_spin_lock |
独占取号,wfe 等待至 owner==我的号 |
取号 O(1),等待 O(队列长) | 标准加锁路径 |
arch_spin_unlock |
owner++ 推进服务号,dsb_sev 唤醒 |
O(1) | 释放锁 |
arch_spin_trylock |
原子判空闲并取号;有竞争直接返回失败 | O(1),不自旋 | 无阻塞尝试加锁 |
三者共享
ldrex/strex独占对与1<<TICKET_SHIFT取号布局,差异仅在"是否等待"与"是否推进 owner"。
五、内存屏障:smp_mb 的位置
| 位置 | 屏障 | 作用 |
|---|---|---|
arch_spin_lock 末尾 |
smp_mb() |
获取后,临界区读写不重排到获锁之前 |
arch_spin_unlock 开头 |
smp_mb() |
释放前,临界区读写不重排到释放之后 |
两者配对构成 ACQUIRE/RELEASE 语义:保证同一把锁保护的临界区在所有 CPU 眼中的内存可见性顺序一致。ldrex/strex 本身保证取号的原子性,但不 保证临界区内存操作的顺序--顺序由 smp_mb 兜底。
六、设计哲学小结
- 排队替代竞争:票号锁把 N 个 CPU 的获取竞争变成按号排队,公平性(FIFO)与可扩展性(释放时 O(1) 唤醒)兼得。
- 数据布局服务指令 :next/owner 同宽打包进 32 位,使一次
ldrex/strex同时读改两者,且trylock能用一条ROR减法判空闲。 - 独占监视替代总线锁 :
ldrex/strex只监视缓存行、不锁总线,多核扩展性优于swp。 - 低功耗等待 :
wfe让等待者睡眠,dsb_sev精准唤醒,避免忙轮询浪费 CPU 与功耗。 - 屏障在边界 :
smp_mb放获取末尾与释放开头,构成 ACQUIRE/RELEASE,把原子性(指令层)与可见性(内存序层)分离。
这些原则与篇一的通用层呼应:架构层用最少的原语(独占对 + 屏障 + 事件)实现公平、高效、低功耗的自旋,上层完全复用。
附录 A:延伸阅读
arch/arm/include/asm/spinlock.h、spinlock_types.h:本文参考实现- 本系列篇一:spinlock 通用层调用链与 SMP/UP 分派
- ARM Architecture Reference Manual:
ldrex/strex/wfe/sev指令语义 arch/x86/:x86 的 qspinlock(MCS 派生),更高竞争下的扩展方案Documentation/locking/:lock子系统总览与各架构实现索引
版权声明(重申)
本文所有内容(包括文字、图表、代码注释与设计分析)均为作者独立创作,著作权归作者所有。
未经作者书面授权,严禁转载、摘编、复制、建立镜像、用于任何商业目的。
Copyright © 2026. All rights reserved. Unauthorized reproduction, distribution, or commercial use is strictly prohibited.