3. 自旋锁回顾:spinlock的实现原理、acquire与release的原子性保证
在深入 sleep/wakeup 机制之前,必须彻底理解 xv6 中最基础的同步原语——自旋锁(spinlock)。自旋锁是 xv6 内核实现临界区保护、防止并发竞态的核心工具,也是 sleep/wakeup 内部实现的重要依赖。
3.1 自旋锁的核心思想
自旋锁是一种忙等待(busy-waiting)锁。当线程尝试获取一个已被其他 CPU 持有的锁时,它会在一个紧凑的循环中反复检查锁的状态,直到锁被释放。这种设计适用于临界区极短的场景,避免了线程切换的开销。
关键特性:
- 忙等待:获取锁失败时,CPU 不会睡眠,而是持续轮询。
- 原子操作:锁的获取与释放必须依赖硬件提供的原子指令(如 RISC-V 的
amoswap)。 - 禁止中断:在持有锁期间,必须关闭本地 CPU 的中断,防止死锁。
3.2 xv6 自旋锁的数据结构
xv6 的自旋锁定义在 spinlock.h 中,结构体如下:
// kernel/spinlock.h
struct spinlock {
uint locked; // 锁状态:0 表示未锁定,1 表示已锁定
char *name; // 锁的名称(调试用)
struct cpu *cpu; // 当前持有锁的 CPU(调试用)
};
| 字段 | 类型 | 说明 |
|---|---|---|
locked |
uint |
锁的核心标志位,0 表示空闲,1 表示已被某 CPU 持有 |
name |
char * |
锁的名称,用于调试和 panic 信息输出 |
cpu |
struct cpu * |
指向当前持有锁的 CPU 结构体,用于检测重复加锁或死锁 |
3.3 acquire 与 release 的实现
xv6 的 acquire 和 release 函数位于 spinlock.c 中,它们共同保证了临界区的互斥访问。
3.3.1 acquire:获取锁
// kernel/spinlock.c
void acquire(struct spinlock *lk) {
push_off(); // 关闭本地 CPU 中断,并保存中断状态
// 使用原子交换指令尝试获取锁
while (__sync_lock_test_and_set(&lk->locked, 1) != 0)
; // 自旋等待
// 内存屏障:确保后续的临界区操作在获取锁之后执行
__sync_synchronize();
// 记录当前持有锁的 CPU(调试用)
lk->cpu = mycpu();
}
原子性保证的关键:
__sync_lock_test_and_set 是 GCC 内置的原子操作,对应 RISC-V 的 amoswap.w 指令。该指令在硬件层面保证:读取锁的旧值与写入新值 1 是一个不可分割的操作。其他 CPU 无法在中间插入任何操作。
3.3.2 release:释放锁
// kernel/spinlock.c
void release(struct spinlock *lk) {
// 检查是否由当前 CPU 持有
if (lk->cpu != mycpu())
panic("release");
// 内存屏障:确保临界区操作在释放锁之前完成
__sync_synchronize();
// 使用原子操作将锁置为 0
__sync_lock_release(&lk->locked);
// 恢复中断状态
pop_off();
}
重要:
release 中的 __sync_synchronize() 是全屏障(full memory barrier),它保证在释放锁之前,所有对共享数据的修改都已经对其他 CPU 可见。如果没有这个屏障,其他 CPU 可能看到过期的数据。
3.4 原子性保证的硬件基础
自旋锁的原子性依赖于 RISC-V 提供的原子内存操作(AMO)指令。下图展示了 acquire 过程中原子交换的硬件流程:
3.5 中断管理与死锁预防
xv6 自旋锁的一个关键设计是:在持有锁期间必须关闭本地 CPU 的中断。这是为了防止以下死锁场景:
- 场景:CPU A 持有锁 L,此时发生中断,中断处理程序尝试获取锁 L。
- 结果:中断处理程序自旋等待锁 L,而 CPU A 无法继续执行以释放锁 L,形成死锁。
- 解决方案:
acquire调用push_off()关闭中断,release调用pop_off()恢复中断。
push_off / pop_off 的嵌套管理:
xv6 使用一个每 CPU 的计数器
xv6 使用一个每 CPU 的计数器
noff 来跟踪中断关闭的嵌套深度。push_off 增加计数,pop_off 减少计数,只有当计数归零时才真正恢复中断。这允许在持有多个锁时安全地管理中断状态。
3.6 自旋锁的适用场景与局限
| 特性 | 说明 | 适用场景 |
|---|---|---|
| 忙等待 | 获取锁失败时 CPU 空转,不释放 CPU | 临界区执行时间极短(微秒级) |
| 无上下文切换 | 不会触发线程调度,延迟低 | 中断处理程序、内核短路径 |
| 简单可靠 | 实现代码少,依赖硬件原子指令 | 多核 CPU 共享数据保护 |
| 局限性 | 长时间等待浪费 CPU 周期;可能导致优先级反转 | 不适合 I/O 等待或长时间临界区 |
3.7 自旋锁与 sleep/wakeup 的关系
自旋锁是 sleep/wakeup 机制的基础构建块:
- 保护等待队列:sleep 和 wakeup 操作需要修改进程的等待状态和条件变量,这些操作必须用自旋锁保护。
- 避免竞态:在检查条件与进入睡眠之间,必须持有自旋锁,防止 wakeup 在 sleep 注册之前发送。
- 中断安全:sleep 实现中会临时释放自旋锁并关闭中断,wakeup 则需要在持有锁的情况下遍历等待队列。