3. Binder驱动锁依赖图:binder_proc → binder_thread → binder_node 的锁获取顺序与依赖关系
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 驱动在代码层面定义了严格的锁获取顺序,违反该顺序是死锁的主要诱因。规范顺序如下:
- 第一层:
binder_proc->inner_lock(进程级锁) - 第二层:
binder_thread->lock(线程级锁) - 第三层:
binder_node->lock(节点级锁)
任何代码路径如果以相反顺序获取锁,或者跨进程交叉获取锁,都会触发锁依赖循环。
3.3 锁依赖关系图
下图展示了 Binder 驱动中三个核心锁之间的依赖关系,以及可能形成死锁的循环路径:
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 锁依赖图的实际排查方法
当系统发生死锁时,可以通过以下步骤绘制锁依赖图并定位问题:
- 收集锁信息: 通过
/proc/binder/proc和/proc/binder/threads获取当前所有进程和线程的锁状态。 - 识别等待关系: 查看每个线程当前持有的锁和正在等待的锁,建立等待图。
- 检测循环: 使用拓扑排序或深度优先搜索检测等待图中是否存在环。
- 定位违规路径: 找到环中违反锁顺序规范的边,即反向获取锁的代码路径。
实战技巧: 在调试 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/接口可以手动排查死锁。