4. 中断与锁:中断禁用与启用(intr_on/intr_off)、中断处理程序中的锁使用规则、锁与中断的交互
在 xv6 内核中,中断与锁的交互是导致死锁的最常见根源之一。中断处理程序(Interrupt Handler)可以随时抢占正在运行的进程或内核线程,如果此时被抢占的代码正持有某个锁,而中断处理程序又试图获取同一个锁,就会产生 单 CPU 自死锁(Single-CPU Self-Deadlock)。本章深入分析中断禁用/启用机制、中断处理程序中的锁使用规则,以及锁与中断的交互模型。
4.1 中断禁用与启用:intr_on / intr_off
xv6 在 RISC-V 架构上通过 intr_off() 和 intr_on() 两个内联函数控制当前 CPU 的中断使能状态。它们直接操作 sstatus 寄存器的 SIE(Supervisor Interrupt Enable)位。
| 函数 | 操作 | 效果 |
|---|---|---|
intr_off() |
清除 sstatus.SIE 位 |
禁用当前 CPU 的所有中断(时钟、设备、软件中断) |
intr_on() |
设置 sstatus.SIE 位 |
启用当前 CPU 的所有中断 |
intr_get() |
读取 sstatus.SIE 位 |
返回当前中断使能状态(0=禁用,1=启用) |
push_off() 和 pop_off() 是中断禁用/启用的嵌套封装。它们维护一个每 CPU 的计数器 mycpu()->noff,确保中断在嵌套锁释放完毕后才被重新启用,避免过早启用中断导致锁状态不一致。
下面的 SVG 展示了中断禁用与启用对锁获取流程的影响:
4.2 中断处理程序中的锁使用规则
xv6 的中断处理程序(如 devintr()、clockintr())运行在中断上下文中,它们必须遵守严格的锁使用规则,否则会破坏内核数据结构的完整性。
4.2.1 三条黄金规则
- 规则一:中断处理程序只能获取“中断安全”的锁。 如果一个锁可能被中断处理程序获取,那么所有普通代码在获取该锁之前必须禁用中断。
- 规则二:中断处理程序不能睡眠或阻塞。 中断上下文没有进程结构,不能调用
sleep()或等待锁时让出 CPU。 - 规则三:中断处理程序必须尽快完成。 长时间持有锁会延迟其他 CPU 或中断的执行,影响系统实时性。
uartintr() 中,如果开发者在未禁用中断的情况下获取 uart_tx_lock,而普通代码路径也获取同一锁,就会导致死锁。xv6 的 uartputc() 在获取锁前调用 acquire(&uart_tx_lock),而 acquire 内部会调用 push_off() 禁用中断,从而避免此问题。
4.2.2 中断处理程序锁使用流程
下面的 SVG 展示了 xv6 中一个典型的中断处理程序锁获取流程:
4.3 锁与中断的交互模型
锁与中断的交互在 xv6 中通过 push_off() / pop_off() 机制实现。理解这个交互模型是调试死锁的关键。
4.3.1 push_off / pop_off 机制
xv6 的 acquire() 和 release() 内部自动调用中断禁用/启用函数,形成嵌套保护:
// kernel/spinlock.c
void acquire(struct spinlock *lk) {
push_off(); // 禁用中断 + 计数器 +1
if (holding(lk))
panic("acquire");
while (__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
__sync_synchronize();
lk->cpu = mycpu();
}
void release(struct spinlock *lk) {
if (!holding(lk))
panic("release");
lk->cpu = 0;
__sync_synchronize();
__sync_lock_release(&lk->locked);
pop_off(); // 计数器 -1,若为 0 则启用中断
}
noff 的作用: 当代码嵌套获取多个锁时,push_off() 每次将 mycpu()->noff 加 1,pop_off() 每次减 1。只有 noff 回到 0 时,才真正调用 intr_on() 启用中断。这保证了在持有任何锁期间,中断始终被禁用。
4.3.2 锁与中断交互的状态机
下面的 SVG 展示了锁与中断交互的完整状态转换:
4.3.3 中断与锁交互的典型场景
| 场景 | 描述 | 是否安全 |
|---|---|---|
| 普通代码获取锁,中断处理程序获取同一锁 | 普通代码在 acquire() 中禁用中断,中断处理程序无法抢占 |
安全 |
| 普通代码持有锁,中断处理程序获取不同锁 | 中断处理程序获取另一个锁,不会产生死锁 | 安全 |
| 普通代码未禁用中断,中断处理程序获取同一锁 | 中断处理程序自旋等待锁释放,但锁永远不会释放 | 死锁 |
| 两个 CPU 分别持有锁 A 和 B,互相等待 | 经典多 CPU 死锁,与中断无关 | 死锁 |
4.4 调试技巧:中断与锁死锁的识别
当 xv6 发生中断相关的死锁时,系统会表现为某个 CPU 持续自旋,无法响应任何中断。以下是调试步骤:
- 检查
sstatus.SIE位: 如果中断被禁用且 CPU 在acquire()循环中,说明可能发生了中断死锁。 - 查看
mycpu()->noff计数器: 如果noff > 0,说明中断被禁用,且锁尚未释放。 - 检查锁的持有者: 通过
lk->cpu查看锁被哪个 CPU 持有,以及该 CPU 当前执行的代码位置。 - 使用 QEMU 的
info registers: 在 QEMU 监视器中查看sstatus寄存器的值,确认中断状态。
info cpus 查看每个 CPU 的当前指令地址,然后使用 elf2hex 或 addr2line 将地址转换为源代码行号,定位死锁位置。
4.5 总结
中断与锁的交互是 xv6 内核设计中最为精妙也最容易出错的部分。通过 push_off() / pop_off() 机制,xv6 确保了在持有任何自旋锁期间,当前 CPU 的中断被禁用,从而避免了单 CPU 自死锁。理解这一机制,是掌握 xv6 并发控制和死锁调试的基础。