3. Binder驱动锁依赖图

Binder 驱动中的死锁风险,根源在于多个锁之间的循环等待。要排查死锁,首先必须理清驱动内部三个核心数据结构——binder_proc、binder_thread、binder_node——之间的锁获取顺序与依赖关系。本章将绘制完整的锁依赖图,并分析每种锁的获取场景与潜在风险。

3.1 核心数据结构与锁定义

在 Binder 驱动中,每个进程、线程和节点都拥有独立的锁。下表列出三个核心结构体及其对应的锁字段:

数据结构 锁字段 锁类型 保护内容
binder_proc inner_lock mutex(可递归) 进程内线程列表、节点列表、待处理事务
binder_thread lock mutex(不可递归) 线程状态、待处理事务、等待队列
binder_node lock spinlock 节点引用计数、强弱指针状态
关键区别: binder_proc->inner_lock 是可递归锁,允许同一线程多次获取;而 binder_thread->lock 是不可递归的,重复获取会导致死锁。binder_node->lock 是自旋锁,不能睡眠,获取后必须快速释放。

3.2 锁获取顺序规范

Binder 驱动在代码层面定义了严格的锁获取顺序,违反该顺序是死锁的主要诱因。规范顺序如下:

  1. 第一层:binder_proc->inner_lock(进程级锁)
  2. 第二层:binder_thread->lock(线程级锁)
  3. 第三层:binder_node->lock(节点级锁)

任何代码路径如果以相反顺序获取锁,或者跨进程交叉获取锁,都会触发锁依赖循环。

3.3 锁依赖关系图

下图展示了 Binder 驱动中三个核心锁之间的依赖关系,以及可能形成死锁的循环路径:

Binder驱动锁依赖图 binder_proc inner_lock (mutex) binder_thread lock (mutex) binder_node lock (spinlock) 获取 获取 死锁循环:反向获取 锁依赖规则 1. 规范顺序:proc->inner_lock → thread->lock → node->lock 2. 禁止反向:node->lock → thread->lock 或 thread->lock → proc->inner_lock 3. 跨进程场景:同时持有 A 进程 proc 锁和 B 进程 thread 锁时需格外小心 正常路径(安全) 1. binder_ioctl → 获取 proc->inner_lock 2. 查找/创建 thread → 获取 thread->lock 3. 操作 node → 获取 node->lock 4. 释放顺序:node → thread → proc 死锁路径(危险) 1. 线程A持有 proc1->inner_lock 2. 线程A尝试获取 proc2->thread->lock 3. 线程B持有 proc2->thread->lock 4. 线程B尝试获取 proc1->inner_lock → 死锁

3.4 锁获取场景分析

在实际的 Binder 驱动代码中,锁的获取分散在多个函数中。下表列出最常见的锁获取场景及其顺序:

场景 入口函数 锁获取顺序 风险等级
Binder 命令处理 binder_ioctl proc → thread 低
事务发送 binder_transaction proc → thread → node 中
节点引用操作 binder_inc_ref node → proc 高(反向)
线程状态切换 binder_thread_read thread → proc 高(反向)
跨进程事务转发 binder_transaction proc1 → proc2 → thread2 极高
高风险场景: 在 binder_inc_ref 中,驱动先获取 binder_node->lock,然后尝试获取 binder_proc->inner_lock。如果此时另一个线程已经持有 proc->inner_lock 并等待 node->lock,就会形成死锁。这是 Binder 驱动中最经典的死锁模式。

3.5 锁依赖的代码级验证

以下是从内核源码中提取的关键锁获取路径,展示了规范顺序与违规顺序的对比:

/* 规范顺序:binder_transaction 中的锁获取 */
static void binder_transaction(struct binder_proc *proc,
                               struct binder_thread *thread,
                               struct binder_transaction_data *tr)
{
    /* 第一层:获取 proc 锁 */
    mutex_lock(&proc->inner_lock);

    /* 第二层:获取目标线程锁 */
    struct binder_thread *target_thread = ...;
    mutex_lock(&target_thread->lock);

    /* 第三层:获取 node 锁(如果需要操作节点) */
    struct binder_node *node = ...;
    spin_lock(&node->lock);

    /* ... 事务处理 ... */

    /* 释放顺序与获取相反 */
    spin_unlock(&node->lock);
    mutex_unlock(&target_thread->lock);
    mutex_unlock(&proc->inner_lock);
}

/* 违规顺序:binder_inc_ref 中的反向获取 */
static int binder_inc_ref(struct binder_node *node, ...)
{
    /* 先获取 node 锁 */
    spin_lock(&node->lock);

    /* 尝试获取 proc 锁 —— 违反顺序规范! */
    mutex_lock(&node->proc->inner_lock);  /* 潜在死锁点 */

    /* ... */

    mutex_unlock(&node->proc->inner_lock);
    spin_unlock(&node->lock);
}

3.6 锁依赖图的实际排查方法

当系统发生死锁时,可以通过以下步骤绘制锁依赖图并定位问题:

  1. 收集锁信息: 通过 /proc/binder/proc 和 /proc/binder/threads 获取当前所有进程和线程的锁状态。
  2. 识别等待关系: 查看每个线程当前持有的锁和正在等待的锁,建立等待图。
  3. 检测循环: 使用拓扑排序或深度优先搜索检测等待图中是否存在环。
  4. 定位违规路径: 找到环中违反锁顺序规范的边,即反向获取锁的代码路径。
实战技巧: 在调试 Binder 死锁时,可以在 binder_transaction 和 binder_inc_ref 等关键函数入口添加 mutex_trylock 检测,如果获取失败则打印当前锁的持有者信息,快速定位死锁源头。

3.7 锁依赖图与死锁检测工具

Linux 内核提供了 lockdep 工具,可以自动检测锁依赖关系并报告潜在死锁。在 Binder 驱动中启用 lockdep 后,如果出现违规的锁获取顺序,内核会输出类似以下的警告:

[  123.456789] ======================================================
[  123.456790] WARNING: possible circular locking dependency detected
[  123.456791] ------------------------------------------------------
[  123.456792] binder:1234 is trying to acquire lock:
[  123.456793]  (&proc->inner_lock){+.+...}, at: binder_inc_ref+0x...
[  123.456794] but task is already holding lock:
[  123.456795]  (&node->lock){+.-...}, at: binder_inc_ref+0x...
[  123.456796] which lock already depends on the new lock.
[  123.456797] -> #0 (&proc->inner_lock){+.+...}:
[  123.456798] -> #1 (&node->lock){+.-...}:
[  123.456799]  Possible unsafe locking scenario:
[  123.456800]        CPU0                    CPU1
[  123.456801]        ----                    ----
[  123.456802]   lock(&node->lock);
[  123.456803]                                lock(&proc->inner_lock);
[  123.456804]                                lock(&node->lock);
[  123.456805]   lock(&proc->inner_lock);
[  123.456806]  *** DEADLOCK ***

通过 lockdep 的输出,可以清晰看到锁依赖链和循环路径,直接对应本章绘制的锁依赖图。

3.8 本章小结

  • 锁依赖顺序: binder_proc->inner_lock → binder_thread->lock → binder_node->lock,不可违反。
  • 高风险函数: binder_inc_ref 和 binder_thread_read 中存在反向获取锁的代码路径,是死锁的高发区。
  • 跨进程场景: 同时操作多个进程的锁时,必须保持全局一致的获取顺序,否则极易形成循环等待。
  • 检测手段: 使用 lockdep 工具可以自动检测锁依赖违规,配合 /proc/binder/ 接口可以手动排查死锁。