3. spinlock 释放与自旋:解锁、自旋等待与中断控制

spinlock 的释放与自旋是内核并发控制的核心环节。xv6 通过 release 函数完成解锁,利用自旋等待机制实现忙等,并借助 push_off / pop_off 管理本地中断状态,防止死锁。本章深入分析这三个机制的设计原理与实现细节。

3.1 解锁函数 release 的实现

release 函数负责释放一个已被当前 CPU 持有的 spinlock。其核心操作包括:检查持有者、关闭中断、清除锁字段、恢复中断状态。

// kernel/spinlock.c
void
release(struct spinlock *lk)
{
  if(!holding(lk))
    panic("release");

  // 在清除锁之前,必须确保所有临界区内的写操作对其他 CPU 可见
  __sync_synchronize();

  // 清除锁的持有者信息
  lk->cpu = 0;

  // 使用原子操作清除锁标志位
  __sync_lock_release(&lk->locked, 0);

  // 恢复调用 acquire 之前的中断状态
  pop_off();
}
关键点:
  • 持有者检查:调用 holding(lk) 验证当前 CPU 确实持有该锁,防止重复释放或释放未持有的锁。
  • 内存屏障:__sync_synchronize() 保证临界区内的所有写操作在锁释放前对其他 CPU 可见。
  • 原子释放:__sync_lock_release 将 lk->locked 原子地置为 0,允许其他 CPU 获取该锁。
  • 中断恢复:pop_off() 恢复 acquire 时保存的中断状态,确保中断控制正确嵌套。

3.2 自旋等待机制

当 CPU 尝试获取一个已被其他 CPU 持有的 spinlock 时,xv6 采用忙等(busy-waiting)策略:CPU 在一个紧凑循环中反复检查锁的状态,直到锁被释放。这种机制适用于短临界区,避免了上下文切换的开销。

// kernel/spinlock.c
void
acquire(struct spinlock *lk)
{
  push_off(); // 关闭本地中断

  // 自旋等待:原子地测试并设置锁标志
  while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
    ;

  // 内存屏障:保证锁获取后,临界区内的读操作不会在锁获取之前执行
  __sync_synchronize();

  // 记录当前 CPU 为锁的持有者
  lk->cpu = mycpu();
}
自旋等待的适用场景:xv6 的 spinlock 自旋等待假设临界区执行时间极短(通常只有几条指令)。如果临界区过长,自旋等待会浪费大量 CPU 周期,此时应使用睡眠锁(sleeplock)。

自旋等待的核心是 __sync_lock_test_and_set 原子操作,它在一个指令周期内完成“读取旧值 → 设置新值 → 返回旧值”的序列。当返回值为 0 时,表示锁未被持有,当前 CPU 成功获取锁;否则继续循环。

自旋等待机制流程图 尝试获取锁 原子 test_and_set 锁已被持有? 获取成功,进入临界区 执行临界区代码 是(继续自旋) 否(获取成功) 自旋等待特点 • 忙等,不释放 CPU • 适用于短临界区 • 原子操作保证无竞争

3.3 push_off 与 pop_off 的作用

push_off 和 pop_off 是 xv6 中管理本地中断状态的一对函数,用于在 spinlock 操作期间保证中断控制的正确嵌套。它们解决了以下关键问题:

函数 调用时机 核心操作 作用
push_off acquire 入口 保存当前中断状态,关闭中断 防止在自旋等待或临界区内被中断打断,避免死锁
pop_off release 出口 恢复之前保存的中断状态 确保中断控制正确嵌套,不破坏外部中断状态
// kernel/riscv.h
static inline void
push_off(void)
{
  int old = intr_get();       // 保存当前中断状态
  intr_off();                 // 关闭中断
  if(mycpu()->noff == 0)
    mycpu()->intena = old;    // 仅在首次关闭时保存状态
  mycpu()->noff += 1;         // 嵌套计数 +1
}

static inline void
pop_off(void)
{
  struct cpu *c = mycpu();
  if(intr_get())
    panic("pop_off - interruptible");
  if(c->noff < 1)
    panic("pop_off - noff underflow");
  c->noff -= 1;               // 嵌套计数 -1
  if(c->noff == 0 && c->intena)
    intr_on();                 // 仅在完全退出时恢复中断
}
死锁风险:如果在持有 spinlock 时发生中断,而中断处理程序尝试获取同一个 spinlock,则会导致单 CPU 死锁。xv6 通过 push_off 在获取锁时关闭中断,彻底杜绝此类问题。

3.4 嵌套锁与中断状态管理

xv6 允许一个 CPU 嵌套获取多个 spinlock(例如:获取锁 A 后,再获取锁 B)。push_off / pop_off 通过嵌套计数器 noff 和中断使能标志 intena 实现正确的状态管理:

  • 首次获取锁:noff 从 0 变为 1,保存当前中断状态到 intena,然后关闭中断。
  • 嵌套获取锁:noff 递增,不再修改 intena,保持中断关闭状态。
  • 嵌套释放锁:noff 递减,当 noff 回到 0 时,根据 intena 决定是否重新开启中断。
push_off / pop_off 嵌套管理示意图 时间→ acquire(A) acquire(B) release(B) release(A) noff: 0→1 noff: 1→2 noff: 2→1 noff: 1→0 intena=old intena不变 intena不变 恢复中断 中断关闭期间(intr_off) 关键规则:只有最外层的 release 才可能真正恢复中断状态 内层 release 仅递减 noff,不改变中断状态

3.5 自旋等待与中断控制的协同

spinlock 的自旋等待与中断控制通过 acquire / release 紧密协同:

阶段 中断状态 自旋行为 风险控制
自旋等待 关闭 忙等,反复 test_and_set 防止中断处理程序获取同一锁导致死锁
临界区执行 关闭 不等待,直接执行 保证临界区原子性,不被中断干扰
锁释放后 恢复原状态 无 允许中断正常处理,不破坏系统响应性
调试建议:当遇到 spinlock 相关的死锁时,首先检查 push_off / pop_off 的嵌套计数是否匹配。使用 print 输出 mycpu()->noff 和 mycpu()->intena 的值,可以快速定位中断状态管理异常。

通过 release 的原子释放、自旋等待的忙等策略以及 push_off / pop_off 的中断管理,xv6 构建了一个简单而高效的 spinlock 机制,为内核并发控制提供了坚实基础。