Skip to content

Binder线程耗尽排查

从 waiting、requested、executing、transaction_stack 和 dumpsys/debugfs 状态定位 Binder 线程池耗尽、阻塞与死锁。

基于android-17.0.0_r1
AndroidBinder调试

Binder线程耗尽排查 ​

“Binder 调用超时”可能来自线程池上限、同步调用链、锁环、oneway backlog 或服务业务阻塞。下面的判别图要求先读取线程与队列状态,再决定进入哪条源码链,避免看到 waiting thread 为零就直接宣布线程池耗尽。

本文收束线程并发小节,承接 Binder线程上限、Binder同步调用、Binder嵌套调用 和 Binder锁竞争。目标不是给出“把线程数调大”的建议,而是从源码指标区分四种现象:线程池真的达到上限、所有线程同步等待下游、oneway 队列堆积、服务线程仍在执行但用户态 starvation。每种现象的恢复点不同。

1. 四类计数 ​

1.1 内核线程 ​

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

debugfs proc 输出:

text
threads: N
requested threads: pending+started/max
ready threads: R
free async space S

threads 是已注册到 proc 的 looper 数;requested_threads 是已发出但还未 BC_REGISTER_LOOPER 的请求;requested_threads_started 是驱动请求并已注册的线程;max_threads 是懒启动上限;ready_threads 来自 waiting_threads,表示可以立即接收 proc work 的线程。

1.2 Native线程 ​

源码文件:frameworks/native/libs/binder/ProcessState.cpp、IPCThreadState.cpp

mCurrentThreads 记录进入线程池的线程,mKernelStartedThreads 记录由 pool 机制启动的线程,mExecutingThreadsCount 记录当前正在执行命令的线程。getThreadPoolMaxTotalThreadCount() 是理论最大值,不等于当前 ready 或 executing 数。

2. 请求为何不增长 ​

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

驱动只有在 requested_threads == 0、waiting_threads 为空、requested_threads_started < max_threads 且当前线程已进入/注册 looper 时才发 BR_SPAWN_LOOPER。因此“没有新线程”可能意味着:已有 idle waiter、请求已经在路上、已经达到上限,或用户态没有处理上一条 spawn 命令。

收到 BC_REGISTER_LOOPER 后,驱动递减 pending request 并递增 started;线程只发送 BC_ENTER_LOOPER 而未收到请求会被标记 invalid。排查时要把 pending+started/max 三个数字分开读。

3. 真实耗尽 ​

当 started == max、ready_threads == 0、多个线程都处于 BR_TRANSACTION 服务执行或 waitForResponse,新同步事务会排在 proc/thread todo 上等待。此时增加 max 只能让后续驱动请求扩容,不能中断已经持有业务锁或等待下游的调用。

4. 同步链阻塞 ​

如果所有 Binder 线程都在同步调用下游,线程可能不在服务方法计算,而是在 IPCThreadState::waitForResponse 阻塞。用 transaction stack 查看 A→B→C 链,检查是否存在 B→A 回调、锁等待或下游服务停止。嵌套原线程复用只适用于有明确栈关系的回调,第三方新入口仍需要空闲线程。

5. Oneway堆积 ​

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

node->async_todo 非空、has_async_transaction 长时间为 true、free async space 下降时,问题可能是单个 Binder node 的异步生产速度超过消费速度,而不是 Binder 线程池全满。检查 buffer 释放是否续接队列,以及是否出现 BR_ONEWAY_SPAM_SUSPECT 或 frozen pending。

6. 饥饿检测 ​

源码文件:frameworks/native/libs/binder/IPCThreadState.cpp

cpp
size_t newThreadsCount = mProcess->mExecutingThreadsCount.fetch_add(1) + 1;
if (newThreadsCount >= mProcess->mMaxThreads)
    mProcess->mStarvationStartTime.compare_exchange_strong(...);
...
if (newThreadsCount < maxThreads && starvationTime > 100ms)
    ALOGE("binder thread pool ... starved ...");

Native 把执行线程数达到上限的时刻记录下来;恢复到上限以下且持续超过 100ms 时记录 starvation 日志。这个日志说明线程池长期没有可用执行槽,但还需结合驱动 ready/transaction 数据判断是服务代码慢还是调度/锁问题。

7. 测试复现 ​

源码文件:frameworks/native/libs/binder/tests/binderLibTest.cpp

ThreadPoolAvailableThreads 先读取理论最大线程数,再用 kKernelThreads + 1 个并发调用占用服务线程,最后增加一个调用验证必须等待释放。测试注释明确指出多一个调用会造成 deadlock;随后释放锁并确认线程池总数恢复到预期值。

该测试还通过延时锁制造“线程在服务中阻塞”的现象,证明耗尽不是线程创建失败,而是所有 worker 都被同步工作占用。

8. 排查顺序 ​

排查现场按以下顺序收集:

  1. proc debugfs:threads、requested threads、ready threads、free async space。
  2. Native 日志:BR_SPAWN_LOOPER、thread pool starved、joinThreadPool 进入/退出。
  3. Binder transaction trace:发送者、目标线程、事务是否 nested/oneway、buffer release。
  4. 服务线程栈:onTransact 业务计算、waitForResponse 下游等待或用户锁。
  5. node async 队列和死亡/frozen 状态。

只看到“调用超时”不足以判断线程耗尽;必须把线程、事务、队列和锁四类观测信息对齐。

9. 恢复策略 ​

  • 达到 max 且业务可并行:在启动前合理提高上限,并验证 buffer/CPU 资源。
  • 同步链或锁环:缩短持锁 IPC、改为 oneway/回调协议或修复调用图,调大线程数只能延后死锁。
  • oneway 队列堆积:降低生产速率、保证消费方法快速释放 buffer,检查 frozen/spam。
  • spawn 请求未注册:检查用户态 BR_SPAWN_LOOPER 处理和 BC_REGISTER_LOOPER,不要手工重复注册。

10. 阅读检查 ​

给定“Binder 调用超时、线程数达到上限、ready=0”的现场,分别构造“所有线程等待下游”“单 node oneway 堆积”“服务计算慢”三种解释,并为每种解释列出下一处源码和应观察的计数。能避免把三者都归因于 maxThreads,才完成线程池耗尽排查。