2. 死锁的定义与经典条件

死锁(Deadlock)是并发编程中最棘手的故障模式之一。在 Linux 内核中,当两个或多个执行线程(进程、中断处理程序、软中断)互相等待对方持有的资源,且任何一方都无法继续推进时,系统便陷入死锁。死锁一旦发生,受影响的线程将永久阻塞,可能导致系统响应完全停止、关键服务崩溃,甚至触发内核 Panic。

要理解死锁的成因与排查方法,必须首先掌握其产生的四个必要条件。这四个条件由 Coffman 等人于 1971 年系统化总结,至今仍是死锁分析与检测的理论基石。

2.1 死锁的四个必要条件详解

死锁的发生必须同时满足以下四个条件,缺一不可:

条件 英文名称 核心含义 内核中的典型表现
互斥 Mutual Exclusion 资源一次只能被一个线程占用 spinlock、mutex、rwlock 等锁的排他性
持有并等待 Hold and Wait 线程已持有一个资源,同时等待获取另一个资源 在持有 spin_lock(A) 时尝试获取 spin_lock(B)
不可剥夺 No Preemption 资源只能由持有者主动释放,不能被强制剥夺 spinlock 不能被其他线程强行解锁
循环等待 Circular Wait 存在一个线程-资源的闭合等待链 T1 等 T2 的资源,T2 等 T1 的资源
关键认知: 破坏任意一个条件即可预防死锁。内核锁机制的设计(如 spin_lock 的不可睡眠、mutex 的优先级继承)以及 lockdep 的检测逻辑,本质上都是在围绕这四个条件做文章。

2.1.1 互斥条件

互斥是锁存在的根本原因。如果资源允许多个线程同时访问,就不需要锁,也就不会因此产生死锁。在内核中,绝大多数经典锁(spinlock、mutex、rwlock 的写锁)都强制互斥访问。当一个线程持有锁时,其他试图获取同一锁的线程必须等待。

互斥条件本身是必要的,无法也不应该被消除——否则锁就失去了意义。死锁排查中,我们关注的是互斥锁与其他三个条件的组合。

2.1.2 持有并等待条件

这是驱动开发中最容易引入死锁的编程模式。线程在已经持有一个锁的情况下,又去请求另一个锁。内核中典型的错误模式如下:

/* 危险模式:持有并等待 */
void thread_A(void)
{
    spin_lock(&lock_A);       /* 持有 lock_A */
    /* ... 临界区操作 ... */
    spin_lock(&lock_B);       /* 等待 lock_B —— 持有并等待 */
    /* ... */
    spin_unlock(&lock_B);
    spin_unlock(&lock_A);
}

如果此时另一个线程以相反的顺序持有 lock_B 并等待 lock_A,死锁便立即形成。lockdep 的核心检测逻辑之一,就是通过记录每个线程的锁获取序列,识别出这种“持有并等待”的交叉依赖。

最佳实践: 尽量使用“一次性获取所有锁”的策略,或严格遵循全局锁顺序。如果无法避免嵌套锁,必须确保所有线程以相同的顺序获取锁。

2.1.3 不可剥夺条件

在内核中,spinlock 和 mutex 默认都是不可剥夺的。持有锁的线程只能通过主动调用解锁原语(spin_unlock / mutex_unlock)来释放资源。其他线程无法强制其交出锁。

这一条件在大多数情况下是合理的——强制剥夺锁会导致临界区数据不一致。但在某些实时场景或死锁恢复机制中,内核提供了可剥夺的变体(如 mutex_trylock、spin_trylock),它们通过“尝试获取,失败则返回”的方式,从编程层面打破了不可剥夺条件。

/* 使用 trylock 打破不可剥夺条件 */
void safe_path(void)
{
    spin_lock(&lock_A);
    if (!spin_trylock(&lock_B)) {
        /* 获取 lock_B 失败,释放 lock_A 后重试或返回 */
        spin_unlock(&lock_A);
        /* 错误处理或延迟重试 */
        return;
    }
    /* 成功持有 lock_A 和 lock_B */
    spin_unlock(&lock_B);
    spin_unlock(&lock_A);
}

2.1.4 循环等待条件

循环等待是死锁最直观的表现形式。它描述了一个闭合的等待链:线程 T1 等待 T2 持有的资源,T2 等待 T3 持有的资源,……,Tn 等待 T1 持有的资源。在内核中,最常见的循环等待是两线程两锁的 AB-BA 模式。

下面的 SVG 图展示了经典的 AB-BA 死锁循环:

循环等待条件:AB-BA 死锁循环 循环等待:AB-BA 死锁循环 线程 T1 线程 T2 资源 A 资源 B 持有 持有 等待 B 等待 A 形成闭合循环等待链:T1→B→T2→A→T1

上图清晰地展示了循环等待的闭合结构:T1 持有 A 并等待 B,T2 持有 B 并等待 A,形成无法打破的等待环。lockdep 在检测到这种循环依赖时,会立即输出死锁警告并打印完整的锁依赖链。

2.2 四个条件的逻辑关系与破坏策略

理解四个条件之间的逻辑关系,有助于在实际开发中快速定位问题并选择正确的修复策略:

  • 互斥 是锁存在的根本,不可消除,但可以通过读写锁、RCU 等机制在特定场景下弱化。
  • 持有并等待 是编程习惯问题,通过锁顺序规范、一次性获取所有锁来避免。
  • 不可剥夺 可通过 trylock 变体或超时机制打破。
  • 循环等待 是死锁的直接表现,通过全局锁顺序、锁分级来预防。
常见误区: 很多开发者认为只要避免循环等待就能防止死锁。实际上,循环等待只是结果,其根源往往是“持有并等待”与不合理的锁顺序。lockdep 的检测也是从锁获取顺序的逆序依赖入手,而非直接检测循环。

2.3 死锁条件在内核中的实际案例

以下是一个真实世界中触发了四个条件的驱动死锁案例:

/* 驱动 A:网卡驱动中断处理 */
void net_rx_irq(struct net_device *dev)
{
    spin_lock(&dev->rx_lock);       /* 持有 rx_lock */
    /* ... 接收数据包 ... */
    spin_lock(&dev->stats_lock);    /* 等待 stats_lock */
    dev->stats.rx_packets++;
    spin_unlock(&dev->stats_lock);
    spin_unlock(&dev->rx_lock);
}

/* 驱动 B:网卡统计读取(进程上下文) */
void net_get_stats(struct net_device *dev)
{
    spin_lock(&dev->stats_lock);    /* 持有 stats_lock */
    /* ... 准备统计 ... */
    spin_lock(&dev->rx_lock);       /* 等待 rx_lock —— 死锁! */
    /* ... 读取统计 ... */
    spin_unlock(&dev->rx_lock);
    spin_unlock(&dev->stats_lock);
}

在这个案例中:

  • 互斥: rx_lock 和 stats_lock 都是 spinlock,互斥访问。
  • 持有并等待: 两个线程都在持有一个锁的同时等待另一个锁。
  • 不可剥夺: spinlock 不可被强制解锁。
  • 循环等待: 中断处理持有 rx_lock 等 stats_lock,统计线程持有 stats_lock 等 rx_lock。

修复方案是统一锁获取顺序:始终先获取 rx_lock,再获取 stats_lock,从而打破循环等待。

2.4 死锁条件与 lockdep 的对应关系

lockdep 的检测逻辑直接映射到四个必要条件:

lockdep 检测机制 对应的死锁条件 检测原理
锁类(lock class) 互斥 为每个锁分配唯一类,区分不同锁实例
锁依赖链记录 持有并等待 记录每个线程的锁获取序列(A→B→C)
不可剥夺检查 不可剥夺 检测在不可睡眠上下文中获取可睡眠锁
循环依赖检测 循环等待 在锁依赖图中搜索闭合环