3. 死锁的常见类型

死锁并非单一形态,在 Linux 内核驱动开发中,根据锁的获取方式、持有关系以及执行流的不同,死锁通常表现为四种典型类型。理解这些类型是使用 lockdep 进行自动化排查的前提。

3.1 自死锁 (Self-Deadlock / Recursive Deadlock)

自死锁是最简单也最容易犯的错误:同一个执行流(进程或线程)在已经持有一个锁的情况下,再次尝试去获取同一个锁。如果该锁是非递归锁(如普通的 spinlock 或 mutex),则执行流会立即阻塞在自己身上,永远无法继续。

关键特征: 单线程、同一把锁、重复加锁。

以下代码展示了自死锁的典型场景:

// 自死锁示例:同一个函数内重复加锁
void my_function(void)
{
    spin_lock(&my_lock);   // 第一次获取锁,成功
    // ... 临界区代码 ...
    spin_lock(&my_lock);   // 第二次获取同一把锁,自死锁!
    // ... 永远不会执行到这里 ...
    spin_unlock(&my_lock);
    spin_unlock(&my_lock);
}
排查提示: 自死锁通常发生在代码重构或函数嵌套调用时。检查是否存在同一个锁在同一个调用路径上被多次获取。
自死锁 (Self-Deadlock) 流程 线程 A 开始执行 获取锁 L (成功) 再次获取锁 L (阻塞) 线程 A 永远等待

3.2 AB-BA 死锁 (AB-BA Deadlock)

AB-BA 死锁是驱动开发中最经典的死锁模式,也称为循环等待。它涉及两个或两个以上的锁和两个或两个以上的执行流。每个执行流持有一个锁,同时等待对方持有的另一个锁,形成循环依赖。

执行流 持有锁 等待锁
线程 A 锁 L1 锁 L2
线程 B 锁 L2 锁 L1
// 线程 A
void thread_a(void)
{
    spin_lock(&lock1);   // 获取 L1
    // ... 一些操作 ...
    spin_lock(&lock2);   // 等待 L2 (被 B 持有)
    // ... 临界区 ...
    spin_unlock(&lock2);
    spin_unlock(&lock1);
}

// 线程 B
void thread_b(void)
{
    spin_lock(&lock2);   // 获取 L2
    // ... 一些操作 ...
    spin_lock(&lock1);   // 等待 L1 (被 A 持有)
    // ... 临界区 ...
    spin_unlock(&lock1);
    spin_unlock(&lock2);
}
避坑指南: 解决 AB-BA 死锁的核心原则是固定锁的获取顺序。例如,所有代码都先获取 L1 再获取 L2,即可打破循环。
AB-BA 死锁循环依赖 线程 A 线程 B 持有锁 L1 持有锁 L2 等待锁 L2 等待锁 L1 形成循环等待链:A→L1→L2→B→L1

3.3 嵌套锁死锁 (Nested Lock Deadlock)

嵌套锁死锁发生在锁的获取与释放顺序不一致的场景中。虽然不一定是严格的 AB-BA 循环,但由于锁的嵌套层次混乱,导致在特定执行路径上出现死锁。这种类型常见于复杂的驱动状态机或回调函数中。

// 嵌套锁死锁示例:释放顺序与获取顺序不匹配
void driver_ioctl(struct file *filp, unsigned int cmd)
{
    spin_lock(&dev->lock_a);   // 获取外层锁
    if (cmd == CMD_SPECIAL) {
        spin_lock(&dev->lock_b); // 获取内层锁
        // ... 处理特殊命令 ...
        spin_unlock(&dev->lock_a); // 错误:先释放了外层锁!
        // ... 此时 lock_b 仍被持有,但 lock_a 已释放 ...
        spin_unlock(&dev->lock_b);
    } else {
        // ... 普通路径 ...
        spin_unlock(&dev->lock_a);
    }
}
最佳实践: 锁的释放顺序必须严格与获取顺序相反(LIFO)。使用 lockdep 可以自动检测到这种释放顺序异常。
嵌套锁释放顺序异常 正常:获取 A → 获取 B 正常:释放 B → 释放 A 异常:获取 A → 获取 B 异常:释放 A → 释放 B 异常释放顺序可能导致锁状态混乱, 在特定竞争条件下引发死锁。

3.4 递归锁死锁 (Recursive Lock Deadlock)

递归锁死锁与自死锁类似,但发生在允许递归加锁的锁类型(如 Linux 内核中的 mutex_lock_nested 或某些 RT 补丁中的递归 spinlock)上。虽然锁本身允许同一线程多次获取,但如果递归深度不受控制,或者在递归过程中调用了可能阻塞或切换上下文的函数,仍然会导致死锁。

内核警告: 标准 Linux 内核主线中的 spinlock 和 mutex 不支持递归。递归锁死锁通常出现在使用 RT-Preempt 补丁或自定义锁封装时。lockdep 会明确报告递归加锁违规。
// 递归锁死锁示例(假设使用支持递归的锁变体)
void recursive_function(int depth)
{
    if (depth > 5) return;  // 递归深度限制

    mutex_lock(&my_recursive_lock);  // 递归加锁
    // ... 操作共享数据 ...
    recursive_function(depth + 1);   // 递归调用
    mutex_unlock(&my_recursive_lock);
}

// 问题:如果在递归过程中调用了 schedule() 或等待另一个锁,
// 即使锁支持递归,也可能因为上下文切换导致死锁。
死锁类型 涉及执行流 涉及锁数量 典型触发条件
自死锁 1 个 1 把 同一函数或调用链重复加锁
AB-BA 死锁 2 个及以上 2 把及以上 锁获取顺序不一致
嵌套锁死锁 1 个或多个 2 把及以上 锁释放顺序异常
递归锁死锁 1 个 1 把(递归) 递归深度失控或递归中切换上下文
四种死锁类型对比 自死锁 1 线程 1 锁 重复加锁 AB-BA 2+ 线程 2+ 锁 循环等待 嵌套锁 1+ 线程 2+ 锁 释放顺序错乱 递归锁 1 线程 1 锁(递归) 深度/切换 lockdep 检测能力 自死锁、AB-BA、嵌套锁:lockdep 可精确检测并报告调用栈 递归锁:lockdep 可检测递归加锁违规,但深度问题需人工审查