2. Binder驱动锁基础
Binder 驱动是 Linux 内核中的一个字符设备驱动,其核心职责是管理进程间通信的传输、缓存与引用计数。为了在多线程并发访问下保护内部数据结构,驱动定义了四种主要的锁对象:binder_proc 锁、binder_thread 锁、binder_node 锁和 binder_ref 锁。理解这些锁的初始化时机、保护范围以及基本操作,是分析 Binder 死锁的基石。
2.1 锁的宏观分类与保护对象
下表总结了四种锁在内核中的角色、初始化位置以及它们所保护的核心数据结构。这是排查死锁时判断“哪个锁被持有”的依据。
| 锁类型 | 内核结构体 | 初始化函数/位置 | 保护的核心数据 |
|---|---|---|---|
binder_proc 锁 |
struct binder_proc |
binder_open() |
进程内线程列表、节点树、引用表、待处理事务队列 |
binder_thread 锁 |
struct binder_thread |
binder_get_thread() |
线程的待处理事务、等待队列、返回错误状态 |
binder_node 锁 |
struct binder_node |
binder_new_node() |
Binder 实体引用计数、强弱指针、所属 proc |
binder_ref 锁 |
struct binder_ref |
binder_get_ref_for_node() |
引用描述符、节点映射、死亡通知回调 |
2.2 锁的初始化与销毁
每个锁在对应的结构体分配时完成初始化。Binder 驱动使用内核标准的 spinlock(自旋锁)和 mutex(互斥锁)两种原语。其中 binder_proc 锁使用 mutex,允许睡眠;其余三个锁使用 spinlock,用于中断上下文或短临界区。
2.2.1 binder_proc 锁:进程级互斥
在 binder_open() 中,当创建新的 binder_proc 实例时,调用 mutex_init 初始化。该锁保护整个进程的 Binder 状态,包括线程红黑树、节点红黑树和引用红黑树。由于操作可能涉及内存分配或用户态拷贝,使用可睡眠的 mutex。
// drivers/android/binder.c
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
// 初始化进程级互斥锁
mutex_init(&proc->inner_lock); // 早期内核版本为 proc->lock
// ... 其他初始化
filp->private_data = proc;
return 0;
}
2.2.2 binder_thread 锁:线程级自旋锁
当驱动需要处理来自用户态的事务时,通过 binder_get_thread() 查找或创建 binder_thread。锁使用 spin_lock_init 初始化,保护线程的待处理事务链表和等待状态。
static struct binder_thread *binder_get_thread(struct binder_proc *proc)
{
struct binder_thread *thread;
// ... 查找或创建逻辑
thread = kzalloc(sizeof(*thread), GFP_KERNEL);
if (!thread)
return NULL;
// 初始化线程自旋锁
spin_lock_init(&thread->inner_lock);
// 初始化其他字段
thread->proc = proc;
return thread;
}
2.2.3 binder_node 锁:节点引用计数保护
当服务端注册 Binder 服务时,驱动调用 binder_new_node() 创建 binder_node。该锁保护节点的强弱引用计数(internal_strong_refs、local_weak_refs 等),防止并发增加/减少引用导致节点提前释放。
static struct binder_node *binder_new_node(struct binder_proc *proc,
binder_uintptr_t ptr,
binder_uintptr_t cookie)
{
struct binder_node *node;
node = kzalloc(sizeof(*node), GFP_KERNEL);
if (!node)
return NULL;
// 初始化节点自旋锁
spin_lock_init(&node->lock);
// 初始化其他字段
node->ptr = ptr;
node->cookie = cookie;
return node;
}
2.2.4 binder_ref 锁:引用映射保护
当客户端获取 Binder 引用时,驱动通过 binder_get_ref_for_node() 创建 binder_ref。该锁保护引用描述符(handle)到节点的映射关系,以及死亡通知回调链表。
static struct binder_ref *binder_get_ref_for_node(struct binder_proc *proc,
struct binder_node *node)
{
struct binder_ref *ref;
// ... 查找或创建逻辑
ref = kzalloc(sizeof(*ref), GFP_KERNEL);
if (!ref)
return NULL;
// 初始化引用自旋锁
spin_lock_init(&ref->lock);
// 初始化其他字段
ref->proc = proc;
ref->node = node;
return ref;
}
2.3 锁的基本操作:加锁与解锁
在 Binder 驱动中,锁的操作遵循严格的“先 proc 锁,后内部锁”的层级顺序。违反此顺序是导致死锁的常见原因。下面通过一个内联 SVG 图展示锁的嵌套关系与操作流程。
2.4 锁操作的关键函数
下表列出了驱动中与这四个锁相关的核心加解锁函数。在死锁排查时,需要重点关注这些函数的调用栈。
| 锁类型 | 加锁函数 | 解锁函数 | 典型调用路径 |
|---|---|---|---|
binder_proc |
mutex_lock(proc->inner_lock) |
mutex_unlock(proc->inner_lock) |
binder_ioctl() → binder_transaction() |
binder_thread |
spin_lock(thread->inner_lock) |
spin_unlock(thread->inner_lock) |
binder_thread_read() → binder_wait_for_work() |
binder_node |
spin_lock(node->lock) |
spin_unlock(node->lock) |
binder_inc_node() / binder_dec_node() |
binder_ref |
spin_lock(ref->lock) |
spin_unlock(ref->lock) |
binder_inc_ref() / binder_dec_ref() |
binder_transaction() 函数中,驱动需要同时操作源进程的 binder_proc 锁和目标进程的 binder_proc 锁。如果两个进程同时向对方发送事务,且加锁顺序不一致(例如 A 先锁自己再锁 B,B 先锁自己再锁 A),就会触发经典的“锁顺序死锁”。这是 Binder 死锁中最常见的一类。
2.5 锁的调试接口
内核提供了 lockdep 工具来检测潜在的锁依赖死锁。Binder 驱动中,可以通过以下方式查看锁的持有状态:
- /proc/binder/proc:列出所有
binder_proc及其持有的锁状态(需内核开启CONFIG_DEBUG_LOCK_ALLOC)。 - dmesg:当
lockdep检测到可能的死锁时,会输出详细的锁依赖链,包括哪个进程持有了binder_proc锁,正在等待binder_thread锁。 - trace_printk:在关键加解锁点插入跟踪日志,用于分析锁的获取顺序。
/proc/binder/proc 确认哪些进程的 binder_proc 锁被长期持有。然后结合 lockdep 的输出,检查是否存在“proc → thread → node”的锁依赖环。记住:Binder 驱动锁的层级是固定的,任何违反层级顺序的加锁操作都是潜在的 Bug。