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 结构体在内存中的布局以及各字段的作用:

struct spinlock 内存布局 locked uint (4字节) name char * (8字节) cpu struct cpu * (8字节) 字段功能说明 • locked:锁的核心标志,0=未锁定,1=已锁定 • name:锁的名称字符串指针,用于 printf 调试输出 • cpu:指向当前持有锁的 CPU 结构,用于检测重复加锁和死锁 • 总大小:4 + 8 + 8 = 20 字节(含对齐填充实际为 24 字节)

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 初始化流程

  1. 设置锁名称:将传入的 name 字符串指针赋值给 lk->name,便于后续调试时识别锁的身份。
  2. 清除锁定标志:将 lk->locked 置为 0,表示锁当前未被任何 CPU 持有。
  3. 清空 CPU 指针:将 lk->cpu 置为 0(NULL),表示没有 CPU 持有该锁。
注意事项:在 xv6 中,所有 spinlock 必须在使用前通过 initlock 初始化。未初始化的锁其 locked 字段可能包含随机值,导致加锁行为不可预测。

2.2.2 初始化状态转换图

initlock 初始化状态转换 未初始化 locked = 随机值 initlock() 已初始化 locked = 0 初始化后 cpu = NULL name = 已设置

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 自旋等待流程图

acquire 加锁与自旋流程 调用 acquire(lk) push_off() 关中断 holding(lk) 检查 locked == 0 ? 自旋等待 否 是 __sync_synchronize() lk->cpu = mycpu()

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 的中断响应。这是为了防止以下经典死锁场景:

  1. CPU 0 持有锁 L,正在执行临界区代码。
  2. CPU 0 发生定时器中断,中断处理程序尝试获取锁 L。
  3. 由于锁 L 已被 CPU 0 持有,中断处理程序进入自旋等待。
  4. CPU 0 无法从中断处理程序返回继续执行临界区,导致永久死锁。

通过关闭中断,xv6 确保在持有 spinlock 期间不会发生中断,从而避免上述死锁。

2.4 本章小结

组件 核心作用 关键字段/操作
struct spinlock 定义锁的数据结构 locked、name、cpu
initlock() 初始化锁为未锁定状态 设置 locked=0、cpu=0
acquire() 原子性获取锁,自旋等待 关中断、test-and-set、内存屏障