2. 死锁类型:自死锁、AB-BA死锁、递归死锁、嵌套锁死锁在驱动中的典型表现
在 Linux 内核与 Android 驱动开发中,死锁并非单一形态。根据锁的获取方式、持有关系与执行流的不同,死锁可划分为多种类型。理解每种类型在驱动中的具体表现,是精准排查与预防死锁的第一步。本章将深入剖析四种最常见的死锁类型:自死锁、AB-BA 死锁、递归死锁与嵌套锁死锁,并结合驱动场景给出典型示例。
2.1 自死锁 (Self-Deadlock / Recursive Lock without Recursion-Aware)
定义:当一个执行流(进程或线程)试图获取一个它已经持有的非递归锁(Non-Recursive Lock)时,会发生自死锁。执行流被自己阻塞,永远无法继续。
驱动中的典型表现:在中断处理函数或持有锁的回调函数中,再次调用同一个加锁路径。
关键点:Linux 内核默认的 spinlock 和 mutex 是非递归的。如果同一个任务在持有锁后再次尝试获取同一把锁,spinlock 会导致死锁(自旋等待自己释放),mutex 会导致任务进入睡眠等待自己,从而永久阻塞。
驱动避坑指南:在编写设备驱动的 ioctl 或读写函数时,若函数内部调用了另一个可能持有同一锁的辅助函数,务必检查锁的嵌套调用路径,避免自死锁。
2.2 AB-BA 死锁 (Circular Deadlock)
定义:两个或多个执行流各自持有一部分资源,并等待对方持有的资源,形成循环等待。这是最经典的死锁形式。
驱动中的典型表现:两个驱动模块(如 I2C 控制器驱动与 GPIO 扩展驱动)互相调用对方提供的接口,且各自内部使用了不同的锁。
| 执行流 | 持有锁 | 等待锁 |
|---|---|---|
| 任务 A (CPU0) | 锁 L1 (mutex_A) | 锁 L2 (mutex_B) |
| 任务 B (CPU1) | 锁 L2 (mutex_B) | 锁 L1 (mutex_A) |
驱动实例:一个音频驱动在设置 codec 时持有 codec 锁,然后调用 GPIO 驱动设置引脚;GPIO 驱动的实现中又反过来查询 codec 状态并尝试获取 codec 锁。若两个锁的获取顺序不一致,极易触发 AB-BA 死锁。
2.3 递归死锁 (Recursive Deadlock)
定义:当一个函数直接或间接地调用自身,并且在每次调用中都尝试获取同一个非递归锁时,会发生递归死锁。这本质上是自死锁在递归调用场景下的扩展。
驱动中的典型表现:一个驱动中的遍历函数(如遍历设备树子节点)在每次递归调用时都尝试获取同一个 mutex 或 spinlock。
内核差异:Linux 内核的
mutex_lock() 不允许递归。如果驱动需要递归加锁,必须使用 mutex_lock_nested() 并正确传递子类(subclass),或者使用 RT 内核中的 rt_mutex。对于 spinlock,内核提供了 spin_lock_nested() 用于类似场景。
/* 驱动中递归死锁的典型错误示例 */
static void traverse_and_lock(struct device_node *np)
{
mutex_lock(&my_driver_lock); /* 第一次调用:获取锁成功 */
/* ... 处理当前节点 ... */
struct device_node *child;
for_each_child_of_node(np, child) {
traverse_and_lock(child); /* 递归调用:再次尝试获取同一锁 -> 死锁! */
}
mutex_unlock(&my_driver_lock);
}
修复方案:将锁放在递归函数外部,或者在递归函数中使用
mutex_lock_nested() 并指定不同的锁子类(subclass)来打破递归锁依赖。
2.4 嵌套锁死锁 (Nested Lock Deadlock)
定义:当多个锁以不同的顺序被嵌套获取时,即使没有形成明显的循环等待,也可能因为锁的获取顺序不一致而导致死锁。这通常发生在多个驱动层或子系统之间。
驱动中的典型表现:一个驱动在调用另一个驱动的接口时,双方都持有自己的内部锁,且双方都尝试获取对方的锁。
| 场景 | 驱动 A (GPIO) | 驱动 B (I2C) | 结果 |
|---|---|---|---|
| 路径 1 | lock(A) → 调用 B | lock(B) → 处理请求 | 正常 |
| 路径 2 | lock(B) → 调用 A | lock(A) → 处理请求 | 死锁 |
Android 驱动中的高发区:在 Power Management 框架中,一个 regulator 驱动在调节电压时可能调用 clock 驱动,而 clock 驱动的 enable/disable 操作又可能反过来查询 regulator 状态。如果两者都使用 mutex 保护内部状态,且调用路径不统一,嵌套锁死锁极易发生。
2.5 四种死锁类型对比总结
| 死锁类型 | 参与任务数 | 锁数量 | 核心特征 | 驱动典型场景 | 排查难度 |
|---|---|---|---|---|---|
| 自死锁 | 1 | 1 | 任务重复获取已持有的非递归锁 | 中断回调中再次加锁 | 低 |
| AB-BA 死锁 | ≥2 | ≥2 | 循环等待资源 | 跨驱动调用顺序不一致 | 中 |
| 递归死锁 | 1 | 1 | 递归调用中重复加锁 | 设备树递归遍历加锁 | 中 |
| 嵌套锁死锁 | ≥2 | ≥2 | 锁获取顺序不一致导致循环 | Regulator 与 Clock 驱动互调 | 高 |
工程实践要点:在 Android BSP 开发中,建议团队建立“锁获取顺序规范文档”,对所有跨驱动调用的锁进行排序编号。使用
lockdep 内核工具可以在开发阶段自动检测上述所有死锁类型,是驱动稳定性工程的第一道防线。