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);
}
排查提示: 自死锁通常发生在代码重构或函数嵌套调用时。检查是否存在同一个锁在同一个调用路径上被多次获取。
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,即可打破循环。
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 可以自动检测到这种释放顺序异常。
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 把(递归) | 递归深度失控或递归中切换上下文 |