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 过程中原子交换的硬件流程:

acquire 原子交换硬件流程 CPU 核心 内存控制器 锁变量 (locked) ① 读取 locked 旧值 ② 写入新值 1 ③ 返回旧值 原子操作的关键保证 • 步骤①和②在硬件层面是不可分割的: 其他 CPU 或 DMA 设备无法在中间插入操作。 • 如果返回的旧值为 0,表示锁空闲,当前 CPU 成功获取锁。 • 如果返回的旧值为 1,表示锁已被占用,CPU 继续自旋等待。

3.5 中断管理与死锁预防

xv6 自旋锁的一个关键设计是:在持有锁期间必须关闭本地 CPU 的中断。这是为了防止以下死锁场景:

  1. 场景:CPU A 持有锁 L,此时发生中断,中断处理程序尝试获取锁 L。
  2. 结果:中断处理程序自旋等待锁 L,而 CPU A 无法继续执行以释放锁 L,形成死锁。
  3. 解决方案:acquire 调用 push_off() 关闭中断,release 调用 pop_off() 恢复中断。
push_off / pop_off 的嵌套管理:
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 则需要在持有锁的情况下遍历等待队列。