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 成功获取锁;否则继续循环。
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决定是否重新开启中断。
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 机制,为内核并发控制提供了坚实基础。