4. acquire 函数实现:关中断、自旋等待、内存屏障
xv6 的 acquire 函数是自旋锁获取的核心入口,它必须同时解决三个关键问题:中断屏蔽(防止死锁)、自旋等待(忙等直到锁可用)、内存序保证(确保临界区操作不被重排到锁外)。本节逐行拆解其实现。
核心要点: acquire 通过“关中断 + 自旋 CAS + 内存屏障”三合一机制,在无锁化硬件原语之上构建安全的临界区入口。
4.1 函数原型与整体流程
acquire(struct spinlock *lk) 的调用者通常是内核线程或中断处理程序。执行流程分为三个阶段:
- 关中断:保存当前中断状态并关闭中断,防止当前 CPU 被中断抢占导致死锁。
- 自旋等待:利用原子交换指令(
__sync_lock_test_and_set)忙等锁变量从 0 变为 1。 - 内存屏障:在成功获取锁后插入
__sync_synchronize,保证后续临界区操作不会重排到锁获取之前。
4.2 关中断:push_off() 与 pop_off()
xv6 使用 push_off() / pop_off() 对来管理中断状态,而不是直接调用 intr_off()。原因在于 acquire 可能嵌套调用(例如在持有锁 A 时尝试获取锁 B),需要支持引用计数。
| 函数 | 行为 | 关键变量 |
|---|---|---|
push_off() |
保存当前中断状态到 mycpu()->intena,然后关闭中断;如果中断已关,仅递增 mycpu()->noff 计数 |
mycpu()->noff (嵌套深度)mycpu()->intena (原始中断使能位) |
pop_off() |
递减 noff;当 noff 归零时,根据 intena 恢复中断状态 |
同上 |
常见错误: 如果直接调用
intr_off() 而不是 push_off(),在嵌套 acquire 场景下,内层 acquire 关闭中断后,外层 release 时 pop_off() 会错误地恢复中断,导致中断过早打开,破坏锁的原子性。
4.3 自旋等待:__sync_lock_test_and_set
自旋等待的核心是 GCC 内置原子操作 __sync_lock_test_and_set(addr, 1),它等价于 x86 的 xchg 指令:原子性地将 1 写入锁变量,并返回旧值。
// kernel/spinlock.c — acquire 自旋部分
void acquire(struct spinlock *lk) {
push_off(); // 关中断(支持嵌套)
// 自旋等待:原子交换,直到旧值为 0(锁空闲)
while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
; // 忙等
// 内存屏障:保证后续临界区操作不会重排到锁获取之前
__sync_synchronize();
// 记录持有锁的 CPU(调试用)
lk->cpu = mycpu();
}
自旋等待的性能特征:
- 优点:实现简单,无上下文切换开销,适合短临界区。
- 缺点:忙等消耗 CPU 时间片,不适合长时间等待。
- 适用场景:xv6 中临界区通常只有几条指令,自旋等待是合理选择。
4.4 内存屏障:__sync_synchronize
在成功获取锁之后、进入临界区之前,xv6 插入 __sync_synchronize()。这是一个全屏障(full memory barrier),保证:
- 编译器屏障:阻止编译器将临界区的内存访问重排到屏障之前。
- CPU 屏障:在弱内存序架构(如 ARM)上,保证屏障后的读/写操作在屏障前的所有内存操作完成后才可见。
为什么需要内存屏障? 假设没有屏障,编译器或 CPU 可能将临界区内的写操作(如
lk->cpu = mycpu())重排到锁获取之前,导致另一个 CPU 看到不一致的状态。屏障强制了“先获取锁,再访问共享数据”的顺序。
在 x86 架构上,__sync_synchronize() 通常被编译为 mfence 指令(或 lock or 等带锁前缀的指令),提供强顺序保证。在 RISC-V 上,它对应 fence 指令。
4.5 完整 acquire 实现与关键点总结
将上述三部分组合,得到完整的 acquire 函数:
// kernel/spinlock.c — 完整 acquire
void acquire(struct spinlock *lk) {
push_off(); // 关中断(支持嵌套)
// 自旋等待:原子交换,直到锁空闲
while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
// 内存屏障:保证后续操作不会重排到锁获取之前
__sync_synchronize();
// 记录持有锁的 CPU(用于调试和死锁检测)
lk->cpu = mycpu();
}
| 步骤 | 代码 | 作用 | 硬件原语 |
|---|---|---|---|
| 1 | push_off() |
保存中断状态并关中断,防止死锁 | 软件管理(引用计数) |
| 2 | __sync_lock_test_and_set |
原子交换,自旋等待锁空闲 | x86: xchg / RISC-V: amoswap |
| 3 | __sync_synchronize() |
全内存屏障,保证内存序 | x86: mfence / RISC-V: fence |
| 4 | lk->cpu = mycpu() |
记录锁持有者(调试辅助) | 普通写操作(在屏障保护下) |
调试提示: 如果遇到“锁获取后临界区数据不一致”的 bug,优先检查内存屏障是否缺失。在 x86 上由于 TSO(Total Store Order)模型,很多重排问题不会暴露,但在 ARM/RISC-V 上会立即崩溃。xv6 的 RISC-V 移植版严格依赖
__sync_synchronize() 的正确性。