11、Binder通信调试:Binder架构、binder信息、binder transaction、死锁分析

Binder,是Android系统里绕不开的核心话题。说实话,我刚开始接触Android系统时,对Binder的理解就是“进程间通信”。但真正上手调试后才发现,Binder远不止这么简单。它既是通信桥梁,也是性能瓶颈,更是死锁的温床。

今天我们就来聊聊Binder的调试。我会从架构讲起,再到具体的信息查看、事务分析,最后聊聊死锁。嗯,这些都是我踩过的坑,希望能帮你少走弯路。

Binder架构:一张图说清楚

先看一张我画的架构图。Binder其实分三层:应用层、驱动层、内核层。说白了,就是用户空间的进程通过驱动和内核交互。

Binder通信架构 应用层(用户空间) Client进程 ServiceManager Server进程 Binder驱动层(内核空间) binder_proc、binder_node、binder_ref、binder_transaction 内核层(Linux Kernel) 进程调度、内存管理、IPC机制

你想想看,一个App要调用系统服务,比如获取定位,底层就是通过Binder完成的。Client发送请求,Server处理并返回结果,中间全靠Binder驱动调度。

核心要点:Binder通信的本质是内存映射。驱动层通过mmap将接收进程的地址空间映射到内核空间,数据拷贝只发生一次。这也是Binder比传统Socket高效的原因。

查看Binder信息:从/proc入手

调试Binder,我最常用的就是/proc/binder目录。这里藏着所有Binder的秘密。我记得有一次线上出现ANR,就是靠这里的信息定位到问题的。

常用的几个节点:

  • /proc/binder/proc:列出所有注册了Binder的进程
  • /proc/binder/state:全局Binder状态
  • /proc/binder/transactions:当前活跃的事务
  • /proc/binder/stats:统计信息

举个例子,查看某个进程的Binder信息:

# 查看进程1234的Binder信息
cat /proc/1234/binder/proc

# 输出示例
binder proc 1234 (com.example.app)
  pending transactions: 2
  threads: 5
  requested threads: 8
  ready threads: 3
  free async space: 524288

这里有个关键指标:pending transactions。如果这个值持续增长,说明有事务堆积,很可能就是死锁或性能瓶颈。

我的习惯:每次排查ANR时,我都会先看/proc/binder/transactions。如果看到大量WAITING状态的事务,基本可以断定是Binder阻塞了。

Binder Transaction:深入事务细节

Binder事务是通信的基本单元。每个事务都有唯一的ID,包含目标进程、代码、数据等信息。我个人习惯用dumpsys binder来查看事务详情。

# 查看所有Binder事务
adb shell dumpsys binder --transactions

# 输出片段
Binder Transaction:
  transaction 12345: from 1000:1234 to 1001:5678
    code: 0x00000001 (TRANSACTION_TYPE)
    flags: 0x00000000
    data_size: 256 bytes
    offsets_size: 32 bytes
    status: WAITING (timeout: 5000ms)

看到WAITING状态了吗?这就是问题所在。事务在等待对方响应,如果一直等不到,就会超时。超时时间默认是5秒,超过后就会触发ANR。

为什么会等待?常见原因有:

  • Server端处理太慢,比如在主线程做耗时操作
  • Server端死锁,线程被其他锁阻塞
  • Binder线程池耗尽,没有空闲线程处理请求

注意:Binder线程池默认最大16个线程。如果所有线程都在等待,新请求就会排队。我曾经遇到一个案例,某个服务在回调中又调用了自己,导致线程全部阻塞,最终系统重启。

死锁分析:Binder死锁的典型场景

Binder死锁,说白了就是两个进程互相等待对方释放资源。我遇到过最典型的场景是这样的:

  1. 进程A持有锁L1,然后通过Binder调用进程B的服务
  2. 进程B在处理请求时,需要获取锁L1
  3. 但锁L1被进程A持有,进程B只能等待
  4. 进程A也在等待进程B的Binder响应
  5. 于是,两个进程互相等待,死锁形成

用代码表示就是:

// 进程A
synchronized (lock1) {
    // 调用Binder服务
    binderService.doSomething();  // 等待B响应
}

// 进程B
void doSomething() {
    synchronized (lock1) {  // 等待A释放lock1
        // 处理请求
    }
}

这种死锁很难排查,因为表面上看只是ANR。我当时的排查步骤是:

  • 先看/proc/binder/transactions,发现大量WAITING事务
  • 再看/proc/binder/proc,发现两个进程的pending transactions都在增长
  • 最后用dumpsys binder --waiting查看具体等待链

避坑指南:我曾经在项目里遇到一个诡异的死锁,查了两天才发现是AIDL接口中定义了一个回调,而回调里又调用了原服务。说白了就是循环调用。解决方案很简单:Binder调用不要持有锁,回调中不要调用原服务

调试工具与技巧

除了/procdumpsys,还有几个工具我经常用:

工具 用途 常用命令
binder 查看Binder节点和引用 binder --nodes
strace 跟踪Binder系统调用 strace -e trace=binder
perfetto 可视化Binder事务 perfetto --txt -o trace.pb
dumpsys 查看Binder整体状态 dumpsys binder

我个人最喜欢用strace。它可以直接看到Binder调用的耗时。比如:

# 跟踪进程1234的Binder调用
strace -p 1234 -e trace=binder -T

# 输出
binder: ioctl(3, BINDER_WRITE_READ, ...) = 0 <0.005>
binder: ioctl(3, BINDER_WRITE_READ, ...) = 0 <2.345>  // 这个调用花了2.3秒!

看到那个2.3秒了吗?这就是问题所在。Binder调用超过1秒就算异常了,2秒以上基本就是阻塞。

小技巧:如果怀疑是Binder线程池耗尽,可以看/proc/binder/proc中的requested threadsready threads。如果requested threads接近16且ready threads为0,说明线程池满了。

总结一下

Binder调试,说白了就是三件事:看状态、找等待、查死锁。我建议你养成习惯,遇到ANR先看/proc/binder/transactions,再结合dumpsys binderstrace定位具体问题。

嗯,Binder这块内容确实有点多,但掌握了这些调试方法,大部分问题都能迎刃而解。记住一点:Binder调用不要持有锁,回调不要循环调用。这两条原则能帮你避开90%的死锁。


公众号:蓝海资料掘金营,微信deep3321