xv6内核spinlock死锁分析与调试方法 - 第2节
2.1 spinlock 的定义与数据结构
在 xv6 内核中,spinlock(自旋锁)是最基础的同步原语,用于保护共享数据结构的互斥访问。当一个 CPU 核心尝试获取已被其他核心持有的 spinlock 时,它会进入一个紧凑的“自旋”循环,反复检查锁是否已被释放,直到成功获取为止。
2.1.1 struct spinlock 结构体
xv6 的 spinlock 定义在 kernel/spinlock.h 中,其核心数据结构如下:
// Mutual exclusion lock.
struct spinlock {
uint locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
};
| 字段 | 类型 | 说明 |
|---|---|---|
locked |
uint |
锁状态标志:0 表示未持有,1 表示已被持有 |
name |
char * |
锁的名称,用于调试和输出信息 |
cpu |
struct cpu * |
当前持有该锁的 CPU 结构指针,用于死锁检测 |
设计要点:
name 和 cpu 字段仅用于调试目的,在正式生产环境中可以移除以节省内存。xv6 保留它们是为了教学和调试便利。
2.1.2 spinlock 的内存布局
下面用 SVG 图展示 spinlock 结构体在内存中的布局以及各字段的作用:
2.2 spinlock 的初始化函数 initlock
initlock 函数负责将 spinlock 结构体初始化为未锁定状态。其实现位于 kernel/spinlock.c 中:
void
initlock(struct spinlock *lk, char *name)
{
lk->name = name;
lk->locked = 0;
lk->cpu = 0;
}
2.2.1 初始化流程
- 设置锁名称:将传入的
name字符串指针赋值给lk->name,便于后续调试时识别锁的身份。 - 清除锁定标志:将
lk->locked置为 0,表示锁当前未被任何 CPU 持有。 - 清空 CPU 指针:将
lk->cpu置为 0(NULL),表示没有 CPU 持有该锁。
注意事项:在 xv6 中,所有 spinlock 必须在使用前通过
initlock 初始化。未初始化的锁其 locked 字段可能包含随机值,导致加锁行为不可预测。
2.2.2 初始化状态转换图
2.3 spinlock 的加锁函数 acquire
acquire 函数是 spinlock 的核心操作,它实现了一个原子性的“测试并设置”(test-and-set)循环。其实现位于 kernel/spinlock.c:
void
acquire(struct spinlock *lk)
{
push_off(); // 关闭中断,防止死锁
if(holding(lk))
panic("acquire");
// RISC-V 原子操作:amoswap
while(__sync_lock_test_and_set(&lk->locked, 1) != 0)
;
// 内存屏障,保证后续操作在锁获取后可见
__sync_synchronize();
// 记录当前持有锁的 CPU
lk->cpu = mycpu();
}
2.3.1 acquire 执行步骤详解
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | push_off() |
关闭当前 CPU 的中断,避免在自旋过程中被中断处理程序尝试获取同一把锁导致死锁 |
| 2 | holding(lk) |
检查当前 CPU 是否已经持有该锁,如果是则触发 panic,防止递归加锁 |
| 3 | __sync_lock_test_and_set |
原子性地将 lk->locked 设置为 1,并返回旧值。如果旧值为 0,表示成功获取锁;否则继续自旋 |
| 4 | __sync_synchronize() |
插入内存屏障(memory barrier),确保在锁获取之后的所有内存操作不会重排到锁获取之前 |
| 5 | lk->cpu = mycpu() |
记录当前持有锁的 CPU 编号,用于调试和死锁检测 |
2.3.2 acquire 自旋等待流程图
2.3.3 原子操作的关键性
__sync_lock_test_and_set 是 GCC 提供的内建原子操作,在 RISC-V 平台上会被编译为 amoswap 指令。该指令保证:
- 原子性:读取和写入操作不可被中断,其他 CPU 核心无法在中间状态插入操作。
- 互斥性:同一时刻只有一个核心能成功将
locked从 0 变为 1。 - 公平性:xv6 的 spinlock 不保证公平性,等待的核心以未知顺序获取锁。
性能提示:自旋等待会持续占用 CPU 资源。在单核系统或持有锁时间极短的场景下,spinlock 效率很高;但如果锁持有时间较长,应考虑使用睡眠锁(sleeplock)以避免 CPU 空转。
2.3.4 中断关闭的必要性
push_off() 在 acquire 开始时调用,其作用是关闭当前 CPU 的中断响应。这是为了防止以下经典死锁场景:
- CPU 0 持有锁 L,正在执行临界区代码。
- CPU 0 发生定时器中断,中断处理程序尝试获取锁 L。
- 由于锁 L 已被 CPU 0 持有,中断处理程序进入自旋等待。
- CPU 0 无法从中断处理程序返回继续执行临界区,导致永久死锁。
通过关闭中断,xv6 确保在持有 spinlock 期间不会发生中断,从而避免上述死锁。
2.4 本章小结
| 组件 | 核心作用 | 关键字段/操作 |
|---|---|---|
struct spinlock |
定义锁的数据结构 | locked、name、cpu |
initlock() |
初始化锁为未锁定状态 | 设置 locked=0、cpu=0 |
acquire() |
原子性获取锁,自旋等待 | 关中断、test-and-set、内存屏障 |