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 图展示锁的嵌套关系与操作流程。

Binder 驱动锁嵌套层级与基本操作流程 binder_proc mutex_lock(&proc->inner_lock) binder_thread spin_lock(&thread->inner_lock) binder_node spin_lock(&node->lock) binder_ref spin_lock(&ref->lock) 操作步骤说明: 1. 先获取 proc 锁(mutex),确保进程状态稳定。 2. 再获取 thread 锁(spinlock),操作线程事务队列。 3. 最后获取 node/ref 锁(spinlock),修改引用计数或映射。 4. 解锁顺序严格相反:先释放 node/ref 锁,再释放 thread 锁,最后释放 proc 锁。 5. 禁止在持有 thread 锁时再去获取 proc 锁,否则形成 ABBA 死锁。

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。