Skip to content

Binder锁竞争

解析 Binder outer、node、inner 自旋锁与 alloc mutex 的所有权、锁序、事务热路径和竞争定位方法。

基于android-17.0.0_r1
AndroidBinder并发内核

Binder锁竞争 ​

Binder 驱动的锁不能只按“全局锁/局部锁”记忆。事务和引用路径在 proc outer、node、proc inner 与 allocator 锁之间存在明确顺序;需要跨进程时还要按 proc 地址决定先后,避免形成 ABBA。下图给出本文使用的锁序地图。

本文承接 Binder异步队列 和 Binder线程耗尽排查。Binder 锁竞争不能只看“某个 mutex 很慢”:驱动把引用、节点、线程队列和 buffer 分配拆到不同锁,并规定跨锁顺序;事务路径还会在锁外执行用户内存复制和唤醒。本文依据 Android 17 binder.c 顶部锁说明,沿引用查找、异步入队和 buffer 释放三个热路径定位竞争边界。

1. 锁分层 ​

源码文件:kernel/common/drivers/android/binder.c

驱动明确规定三把主自旋锁的顺序:

  1. proc->outer_lock:保护 binder_ref。
  2. node->lock:保护 Binder node 字段。
  3. proc->inner_lock:保护线程/节点列表、todo 队列、transaction_stack 和 node->async_todo。

同一 proc 下不能把低层锁嵌套到另一 proc 的同级或更低层锁之下。函数后缀 _olocked、_nlocked、_ilocked、_nilocked 是源码级锁契约,调用者必须先满足它们。

2. 加锁辅助 ​

源码文件:kernel/common/drivers/android/binder.c

c
static void _binder_proc_lock(struct binder_proc *proc, int line) {
    binder_debug(BINDER_DEBUG_SPINLOCKS, "%s: line=%d\n", __func__, line);
    spin_lock(&proc->outer_lock);
}

static void _binder_node_inner_lock(struct binder_node *node, int line) {
    spin_lock(&node->lock);
    if (node->proc)
        binder_inner_proc_lock(node->proc);
}

这些 wrapper 除了统一 lockdep/调试注释,还让 Sparse 注解知道函数需要或释放哪些锁。binder_node_inner_lock 固定 node→inner 顺序,避免一个路径先 inner 后 node 造成反转。

3. 引用查找 ​

源码文件:kernel/common/drivers/android/binder.c

binder_get_ref_for_node_olocked 在持有目标进程 outer_lock 时查找红黑树。如果 descriptor bitmap 已满,get_ref_desc_olocked 会暂时释放 outer lock 分配更大的 bitmap,再重新获取并重试;注释明确要求调用者把 descriptor 当作可能失效处理。

这段“锁内查找、锁外分配、重试”是避免长时间持有自旋锁的典型模式。不能把 bitmap 分配直接放在 outer lock 内,否则 GFP 分配和跨 CPU 竞争会放大所有 Binder 引用操作的尾延迟。

4. 事务提交 ​

源码文件:kernel/common/drivers/android/binder.c

binder_proc_transaction 先锁 node 更新 has_async_transaction,再锁目标 proc 的 inner lock 选择等待线程、写入 thread/proc/node todo,最后在释放 inner/node 锁后唤醒线程。异步事务只有在 node 锁和目标 inner 锁保护下操作 async_todo;真正的服务执行发生在锁外。

这种分段让事务提交不会持有 node 锁等待用户态服务方法。竞争热点通常是高并发发送同一 node 时的 node lock,或目标进程所有发送者争用同一个 inner lock,而不是服务代码本身的业务锁。

5. Buffer释放 ​

源码文件:kernel/common/drivers/android/binder.c、kernel/common/drivers/android/binder_alloc.c

binder_free_buf 先用 proc inner lock 清理 transaction 指针,再取得 node+inner 锁续接 async_todo,最后调用 binder_alloc_free_buf 获取 alloc->mutex 合并空闲 buffer。node/inner 自旋锁和 alloc mutex 分开,避免在分配器的可能较长临界区内继续持有事务队列锁。

binder_alloc_free_buf 的 clear_on_free 清零操作甚至在拿 alloc mutex 前完成,源码注释说明这样可以减少大 buffer 清零造成的 mutex 竞争;清零不影响链表正确性。

6. 跨进程锁序 ​

驱动顶部规则禁止在 procA 的锁下嵌套 procB 同级或更低锁。事务需要同时访问发送方引用、目标 node 和目标 proc 时,会先获取稳定引用/临时引用,释放发送方锁,再进入目标锁域。违反该规则会形成 A 等 B、B 等 A 的内核死锁,即使用户态调用本身没有循环。

7. 锁外工作 ​

用户地址 copy_from_user/copy_to_user、buffer 页分配、服务线程执行和 wake_up 不应被误读为某一主锁的保护区。锁只保护内核元数据的一致性;把耗时操作移入锁内会让竞争表现为所有事务延迟上升,而不是单个调用变慢。

8. 观测入口 ​

源码文件:kernel/common/drivers/android/binder.c

BINDER_DEBUG_SPINLOCKS 会在 wrapper 中记录加锁位置;binder_transaction、binder_transaction_received、binder_transaction_alloc_buf、binder_transaction_buffer_release tracepoint 可以把锁竞争前后的事务、buffer 和队列状态对齐。debugfs 的锁日志需要结合 trace 时间戳,不能单独作为等待时间证明。

9. 分配器竞争 ​

源码文件:kernel/common/drivers/android/binder_alloc.c

alloc->mutex 保护每个进程的 buffer 空间。大量大 Parcel、频繁 clear-on-free 或 async buffer 释放会让该 mutex 成为热点;但它与 node async 队列锁是两个独立瓶颈。排查时应分别统计 buffer 分配/释放耗时和 node/inner 锁等待,避免把内存碎片问题误判为线程池锁竞争。

10. 调试路径 ​

先确认热点属于 outer、node、inner 还是 alloc mutex;再根据调用栈检查是否在锁内分配内存、访问另一 proc 或执行用户拷贝。事务量高但锁等待低,可能是服务线程耗尽;锁等待高而事务量正常,优先检查同一 node 的引用/async 队列集中度和大 buffer 清理。

11. 阅读检查 ​

从一次同步事务发送分别标出 outer、node、inner、alloc mutex 的获取释放点,并解释为什么 binder_proc_transaction 可以在锁外唤醒/执行服务。再设计 procA↔procB 交叉访问场景,指出顶部锁序规则如何阻止内核锁反转。