4. acquire 函数实现:关中断、自旋等待、内存屏障

xv6 的 acquire 函数是自旋锁获取的核心入口,它必须同时解决三个关键问题:中断屏蔽(防止死锁)、自旋等待(忙等直到锁可用)、内存序保证(确保临界区操作不被重排到锁外)。本节逐行拆解其实现。

核心要点: acquire 通过“关中断 + 自旋 CAS + 内存屏障”三合一机制,在无锁化硬件原语之上构建安全的临界区入口。

4.1 函数原型与整体流程

acquire(struct spinlock *lk) 的调用者通常是内核线程或中断处理程序。执行流程分为三个阶段:

  1. 关中断:保存当前中断状态并关闭中断,防止当前 CPU 被中断抢占导致死锁。
  2. 自旋等待:利用原子交换指令(__sync_lock_test_and_set)忙等锁变量从 0 变为 1。
  3. 内存屏障:在成功获取锁后插入 __sync_synchronize,保证后续临界区操作不会重排到锁获取之前。
acquire 函数三阶段执行流程 阶段 1:关中断 push_off() 阶段 2:自旋等待 while(__sync_lock_test_and_set(...)) 阶段 3:内存屏障 __sync_synchronize() 关键设计意图 • 关中断:防止当前 CPU 在持有锁时被中断,中断处理程序又尝试获取同一把锁 → 死锁 • 自旋等待:利用硬件原子指令保证锁状态切换的原子性 • 内存屏障:禁止编译器/CPU 将临界区内存操作重排到锁获取之前

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 中临界区通常只有几条指令,自旋等待是合理选择。
自旋等待状态机 锁空闲 locked == 0 自旋等待 locked == 1 锁已获取 locked == 1 (本CPU) CAS 失败 → 继续自旋 只有当 __sync_lock_test_and_set 返回 0 时,才退出自旋进入“锁已获取”状态

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() 的正确性。