Binder(六):服务端方法到底在哪条线程执行?

0 阅读17分钟

Binder(六):服务端方法到底在哪条线程执行?

前五篇已经完成:

调用 → 协议 → 对象 → 驱动 → 服务发现

现在客户端已经拿到目标 Binder 引用,并发起一笔事务。驱动把工作放进服务端进程,但还需要一条用户态线程真正执行:

BR_TRANSACTION
    ↓
IPCThreadState::executeCommand()
    ↓
BBinder / JavaBBinder
    ↓
Stub.onTransact()
    ↓
服务端业务方法

接下来要分清的是:

服务端哪条线程在驱动里等待事务?同步、oneway、线程池、Handler、嵌套调用和重入又怎样共同决定执行行为?

先用几条边界约束住讨论:

跨进程 Binder 请求通常先由服务端 Binder 线程池中的一条线程执行入口。
​
同步调用:客户端原线程等待 reply。
oneway:客户端不等待业务 reply,但服务端仍需要 Binder 线程处理。
同一 node 的 oneway 事务串行,但不保证固定在线程池同一线程。
Binder 入口可以再 post 到 Handler;这是服务自身的线程切换策略。
同步嵌套调用会形成事务栈,可能复用调用链中原本等待的线程并产生重入。

源码沿用导读第四节“系列统一基线”,线程语义同时对照 AOSP 官方 Binder threading

先把线程名字说准确

主线程、Binder 线程与 Handler 线程

线程谁创建/管理主要任务
服务端主线程进程运行时 / Looper组件生命周期、主 Looper 任务或 system_server 主循环
服务端 Binder 线程Binder 运行时线程池在驱动中等待并执行远端事务入口
模块 Handler 线程服务代码显式创建或选择串行处理模块内部状态、定时任务和事件

这三种线程可能在同一个进程中同时存在,却不是同一条线程。

Binder 负责:把远端事务交给 Binder 入口线程。
Handler 负责:把服务内部任务投递到某个 Looper 线程。

Binder 不会自动把所有服务端方法切到主线程;Handler 也不负责让事务跨进程。

四种常见执行模型

image.png

读图时比较两个问题:

客户端线程是否等待 reply?
服务端 Binder 线程是否继续承担业务,还是转交 Handler?

四种情况不能都简称为“异步”:

同步 Binder
AIDL oneway
Binder 入口 post 后立即返回
Binder 入口 post 后同步等待

Binder 线程的创建与等待入口

1. Java App / system_server 路径

Android Java 进程从 Zygote 初始化后,会通过 nativeZygoteInit() 进入 AndroidRuntime::onZygoteInit()。Android 16 的 app_main.cpp 中:

virtual void onZygoteInit() {
    sp<ProcessState> proc = ProcessState::self();
    proc->startThreadPool();
}

这会启动 Binder 线程池的 main pooled thread。这里的 main 是 libbinder 线程池角色,不是 Java 应用主线程,也不是主 Looper 线程。

2. 普通 Native daemon 路径

Native Binder 服务通常需要显式组织:

ProcessState::self()->startThreadPool();
​
// 注册 Binder 服务
​
IPCThreadState::self()->joinThreadPool();

AOSP AIDL backends 也明确提醒:AIDL / Binder 不会因为“发送了一个 Binder 对象”就自动为所有 native 后端启动线程池。

3. servicemanager 是一个特殊例子

Android 16 servicemanager 把最大 Binder 线程数设为 0,并通过 BinderCallback 把 Binder fd 接入 Native Looper

ps->setThreadPoolMaxThreadCount(0);
​
sp<Looper> looper = Looper::prepare(false);
BinderCallback::setupTo(looper);
​
while (true) {
    looper->pollAll(-1);
}

这说明“Native 服务端”并不只有一种固定启动模板。所以说讲稳定线程模型时,必须区分普通 libbinder 线程池和把 Binder fd 集成到自有事件循环的特殊服务。

ProcessState 属于进程,IPCThreadState 属于线程

对象作用范围主要职责
ProcessState进程级驱动 fd、mmap、handle 代理缓存、线程池配置
IPCThreadState线程级当前线程输入/输出命令缓冲、transact、调用身份、事务上下文

对象关系是:

一个 Binder 进程
    └─ 主要 ProcessState
          ├─ driver fd
          ├─ Binder mmap / 进程级状态
          ├─ handle → BpBinder 缓存
          └─ 线程池配置
​
每条参与 Binder 的线程
    └─ 自己的 IPCThreadState
          ├─ mIn / mOut
          ├─ transact / waitForResponse
          ├─ calling PID / UID
          └─ 当前事务上下文

因此:

ProcessState::self() 讨论进程级 Binder 状态;
IPCThreadState::self() 讨论当前线程怎样与驱动交互。

joinThreadPool() 让当前线程进入 Binder 循环

Android 16 的 IPCThreadState::joinThreadPool() 先向驱动写:

mOut.writeInt32(
        isMain
        ? BC_ENTER_LOOPER
        : BC_REGISTER_LOOPER);

随后循环:

talkWithDriver()
    ↓
读取 BR_* 命令
    ↓
executeCommand()
    ↓
处理事务、引用、死亡通知或线程池命令
    ↓
再次进入驱动等待

所以 Binder 线程不是在 Java 层轮询一个消息列表。它通过当前线程的 IPCThreadState 长期进入驱动,等待 Binder work。

驱动如何请求线程池增长

进程会向驱动设置最大线程数。驱动发现:

没有空闲线程;
没有待响应的 spawn 请求;
已启动线程数小于 max_threads;

就可以向用户态返回:

BR_SPAWN_LOOPER

Android 16 的 IPCThreadState::executeCommand() 收到后调用 ProcessState::spawnPooledThread(false) 创建新池线程。

两个边界要同时记住:

线程按需增长,不代表每笔事务新建线程;
达到上限后,新的事务仍可能排队等待可用 Binder 线程。

线程池繁忙是第九篇排障模型中的重要“目标进程等待执行”阶段。

执行线程与完成语义

同步调用连接两端线程,却不合并线程上下文

以同步 add() 为例:

客户端调用线程
    BC_TRANSACTION
    ↓
    waitForResponse()
    ↓ 等待 BR_REPLY
​
服务端 Binder 线程
    BR_TRANSACTION
    ↓
    Stub.onTransact()
    ↓
    service.add()
    ↓
    BC_REPLY
​
客户端原线程
    BR_REPLY
    ↓
    从 add() 返回

同步返回的准确语义是:

服务端 Binder 方法已经返回;
reply 已回到客户端。

它不必然表示:

服务端 post 到 Handler 的后续工作已经完成;
其他服务的最终状态已经稳定;
所有异步回调已经发出。

oneway 取消 reply 等待,不负责创建线程

oneway 改变的是调用完成语义:

客户端把异步事务交给内核后即可继续;
客户端不等待服务端业务 reply。

它不承诺:

创建新线程;
立即开始执行;
并行执行;
业务一定成功;
远端已经完成状态变更。

服务端仍然需要某条 Binder 线程读取并执行事务。如果线程池繁忙,oneway 也会在服务端排队。

官方线程文档还指出:接收进程可能死亡或冻结而没有处理这笔调用,调用方也未必得到失败反馈。因此不能把 oneway 方法返回写成“服务端执行成功”。

还要避免另一个过度简化:oneway 不等待服务端业务 reply,不等于 transact() 在所有情况下都绝对零等待、绝不失败。Parcel 编码、进入驱动、buffer 压力和本地同进程退化路径仍在调用方路径上;准确说法是异步事务交给内核后,调用线程不再等待远端业务执行完成

同一 node 的 oneway 保序与线程选择

驱动为每个 binder_node 维护:

has_async_transaction
async_todo

当同一 node 已有 oneway 正在处理时,后续异步事务进入该 node 的 async_todo。上一笔完成释放 buffer 后,驱动再推进下一笔。

因此:

同一 Binder node 的 oneway:不会同时执行;
不同 Binder node 的 oneway:可以由不同线程并行执行。

但“同一 node 串行”不等于:

永远由同一条 Binder 线程执行。

官方 Binder threading:Serialization of asynchronous transactions 明确说明,同一 node 的异步事务可以由线程池不同线程先后执行,只是不同时执行。

所以三个词必须分开:

异步 ≠ 并行
串行 ≠ 固定线程
oneway ≠ 不需要线程安全

把最容易混淆的并发与完成语义压缩成矩阵:

事务类型同一 node 是否可同时执行是否固定 Binder 线程调用方是否等待业务 reply
同步事务条件满足时可以并发
同一 node 的 oneway串行,不同时执行
不同 node 的 oneway可以并发

“条件满足”指同时存在多笔调用、目标线程池有可用线程,并且服务实现没有用锁或单线程 Handler 主动串行化。驱动允许并发不等于保证并发。这张表只描述 Binder 入口;服务内部若再转 Handler,还要另外分析 Handler 队列和完成语义。

服务主动 post Handler 才发生线程转交

Binder 线程适合接收并发 IPC 入口,但很多模块希望内部状态只在一条 Looper 线程串行修改:

@Override
public void updateState(State state) {
    final int uid = Binder.getCallingUid();
    mHandler.post(() -> handleUpdate(uid, state));
}

这里发生两次独立调度:

Binder Driver
    → 选择服务端 Binder 线程执行 IPC 入口
​
Handler
    → 把内部任务排入目标 MessageQueue

Binder 不知道 Handler 队列;Handler 也不知道远端事务路由。

fire-and-forget 与同步等待 Handler 的完成语义

模式 A:post 后立即返回
public void updateState(State state) {
    mHandler.post(() -> applyState(state));
}

时间关系:

Binder 入口完成 post
    ↓
同步 Binder 方法返回 / oneway 入口结束
    ↓
Handler 未来某时执行 applyState

客户端看到 Binder 返回,只能证明“投递被入口接受”,不能证明状态已经应用。

模式 B:post 后同步等待
public Result queryState() {
    return runWithScissorsOrFutureOnHandler();
}

时间关系:

客户端线程等待 Binder reply
    ↓
服务端 Binder 线程等待 Handler
    ↓
目标 Handler 排队或执行

Handler 延迟会沿调用链反向传播成 Binder 慢,最终可能表现为客户端主线程 ANR。

这正是 Binder 第九篇要建立的:

Handler 慢
→ Binder 线程慢
→ 客户端同步调用慢

Messenger 用 Binder 传递 Message,由目标 Handler 串行处理

Messenger 常见模型是:

Binder:跨进程传递 Message
Handler:在目标 Looper 线程串行处理 Message

这里的串行来自目标 Messenger 所绑定的 Handler / Looper,不是 Binder 把所有 Messenger 消息全局串行化。

与 AIDL 的差别主要在业务建模:

AIDL:类型化远程方法;
Messenger:Message 投递协议。

它们底层都可以依赖 Binder,但目标端线程策略不同。Messenger 把 Handler 串行处理直接纳入接口模型;AIDL Stub 默认先运行在 Binder 入口线程,是否转 Handler 由服务实现决定。

调用链折返时:嵌套、重入与死锁

同步嵌套事务形成跨进程调用栈

考虑三笔同步事务:

进程 A,线程 A1
    调用进程 B:T1
​
进程 B,线程 B1 处理 T1
    又调用进程 C:T2
​
进程 C,线程 C1 处理 T2
    又回调进程 A:T3

这不是三个完全无关的消息。驱动维护事务之间的父子和等待关系:

T1
└─ T2
   └─ T3

当调用链重新进入已经参与事务链的进程 A 时,Binder 可以复用原本正在等待 T1 的线程 A1 来处理 T3,以保持类似本地函数递归的语义。

这就是为什么 Binder 不能只按“请求队列”理解。同步事务还具有调用栈式的嵌套关系。

等待中的线程也可能被同一调用链重入

三个概念必须区分:

并行:多条线程同时执行。
并发:多个调用的执行区间重叠。
重入:前一次调用尚未返回,代码路径再次进入同一对象或状态。

image.png

即使进程 A 只有一条 Binder 线程,仍可能:

A1 正在处理本地状态并同步调用 BB 处理时同步回调 A;
驱动复用等待中的 A1;
A1 在原调用尚未结束时再次进入同一对象。

如果代码假设“这段状态修改期间绝不会再次进入”,就会出现迭代器失效、状态机越级、重复提交或非递归锁死。

所以:

只有一条 Binder 线程
≠
不存在并发语义和重入风险

持锁跨 Binder 边界会放大等待环

跨进程双锁死锁
进程 A:持有锁 LA,调用 B
进程 B:持有锁 LB,调用 A

两边都等待对方进入自己当前持有的锁,形成跨进程等待环。

单线程重入问题
A1 持有非递归 native mutex 或正在修改状态;
A1 调用 BB 回调 A;
回调被重入 A1,再次请求同一 mutex 或访问半完成状态。

AOSP 官方线程文档的直接建议是:

不要在持锁时发起 Binder 调用。

因为跨进程代码、版本和回调方向很难维护全局一致的锁顺序。

典型死锁:Binder 线程等 Handler,Handler 再等 Binder

典型等待环:

App 主线程
    同步调用 system_server
    等 Binder reply
​
system_server Binder 线程
    post 到 DisplayThread
    同步等待结果
​
DisplayThread
    持锁调用另一个 Binder 服务
    或等待 App 回调完成

如果 App 回调又需要 App 主线程处理,就可能形成:

App main
→ system_server Binder
→ DisplayThread
→ Binder callback
→ App main

诊断时只看到 App 主线程卡在 transactNative(),不能得出“Binder Driver 慢”。真正瓶颈可能是服务端 Binder 线程等待 Handler,或 Handler 再等待下一笔 Binder。

六个实验 mode 各自证明什么

BinderLab 把各类线程实验拆成独立 mode,避免同一 node 的 oneway backlog 混入 Handler 延迟。每次先 force-stop、清日志,再只启动一个 mode:

$adb = 'D:\Android\platform-tools\adb.exe'
& $adb shell am force-stop com.example.binderdemo
& $adb logcat -c
& $adb shell am start -W `
  -n com.example.binderdemo/.MainActivity `
  --es experiment oneway-cross-node

六个 mode 如下;每一笔实验事务仍用 requestId 关联。死亡实验另有独立的 binder-death mode,不在本篇线程语义表中:

mode回答的问题
handler-latency-baseline不注入长阻塞时,同步 Binder 入口等待 Handler 的基线
handler-latency-blocked注入 1.5 s 队头阻塞后,同一条八点链怎样变化
sync-reentry同步嵌套 callback 是否在本次运行复用原等待线程
oneway-same-node同一 ICalculator node 的三笔 async 区间关系
oneway-cross-nodeN1 与 N2 两个 Binder node 是否存在并发对照
async-callbackBinder 入口、Handler 和异步反向 callback 是否分离
实验 1:同步 addWithRequestId() 等待 Handler

客户端和服务端都打印:

PID
TID
Thread.currentThread().getName()
进入时间
退出时间

第一篇保留最小 add();本实验用 addWithRequestId(requestId, a, b, injectHandlerBlocker) 关联 C0/S0/H0A/H0B/H1/H2/S1/C1。baselineblocked 使用同一协议、同一 Handler 路径和同一分析器,只改变是否在目标任务前注入 1.5 s blocker。预期:两组均由客户端调用线程等待、服务端 Binder 线程执行并在 reply 后恢复;阻塞组的 H0B→H1 应与基线明显分离。

实验 2:同 node 连续 oneway
calculator.notifyValue(1001, 1);
calculator.notifyValue(1002, 2);
calculator.notifyValue(1003, 3);

耗时必须发生在 Binder 入口内部,不能 post 到单线程 Handler 后再观察 Handler 串行:

@Override
public void notifyValue(int requestId, int value) {
    // Lab-only: deliberately blocks a Binder thread.
    // Never copy this into production oneway handlers.
    long begin = SystemClock.elapsedRealtimeNanos();
    log("S_ONEWAY_ENTER", requestId, value, begin, Process.myTid());
    SystemClock.sleep(300);
    long end = SystemClock.elapsedRealtimeNanos();
    log("S_ONEWAY_EXIT", requestId, value, end, Process.myTid());
}

客户端还为每一笔记录 C_SAME_NODE_CALL_BEGIN/RETURN。证据中第二笔调用已经返回时,服务端第一笔仍在执行;三组 ENTER → EXIT 仍不重叠,因此不能再用“第二笔发送得太晚”解释串行。相邻事务的 TID 可以不同;但这组结果单独还不能排除“当时目标进程只有一条活跃 Binder 线程”。若耗时放在同一个 Handler,则更只能证明 MessageQueue 串行,不能证明 Binder 对同一 node 的约束。

实验 3:不同 node 并发对照

服务端创建两个独立 Stub,因此对应两个 Binder node:

interface IAsyncWorker {
    oneway void work(int requestId, int value);
}
​
interface ICalculator {
    IAsyncWorker getAsyncWorker(int workerId);
}

客户端先同步取得 N1、N2,再让两条线程同时发送:

线程 A:N1.work(1001, 1) → N1.work(1002, 2)
线程 B:N2.work(1003, 3) → N2.work(1004, 4)

每一笔调用前后分别记录 C_N1_CALL_BEGIN/RETURNC_N2_CALL_BEGIN/RETURN。两边各发两笔是为了让两个 node 都形成 backlog,不构成“每次必然重叠”的保证。强因果闭环要求:

C_N1_CALL_RETURN(1002) < S_N1_EXIT(1001)
C_N2_CALL_RETURN(1004) < S_N2_EXIT(1003)
N1、N2 各自的两段不重叠
N1 与 N2 至少一段重叠,且服务端 TID 不同
​
由此联合观察到:
目标进程当时确实有至少两条 Binder 线程参与;
同 node 第二笔已经完成客户端 oneway transact 返回;
同 node 的第二笔没有与第一笔并发;
不同 node 的事务可以并发。

客户端 transact 返回不是 Perfetto 里的驱动排队时间戳,但它已经排除 sender 线程直到第一笔结束才调用第二笔的替代解释。不同 node 是“允许并发”,不是“保证每次并发”;某次没有重叠时,应先检查线程池和调度状态,不能反推官方语义错误。

实验 4:Binder 入口转 Handler 与异步 callback

BinderLab 单独提供:

oneway void notifyValueViaHandler(int requestId, int value);

它只在 Binder 入口记录 S_HANDLER_POST,再 worker.post();Handler 执行时记录 S_HANDLER_RUN,并通过 oneway IResultCallback 反向通知 App。分别记录:

Binder 入口返回时间
客户端调用返回时间
Handler 真正执行时间

分析器只把以下稳定偏序作为硬门禁:

C_ASYNC_CALL_BEGIN < C_ASYNC_CALL_RETURN
C_ASYNC_CALL_BEGIN < S_HANDLER_POST < S_HANDLER_RUN < C_CALLBACK < C_ASYNC_CALLBACK_OBSERVED
C_ASYNC_CALL_RETURN < C_ASYNC_CALLBACK_OBSERVED

C_ASYNC_CALL_RETURN 与 post/run/callback 的其他先后受两端并发调度影响;证据日志中 return 早于 post,但这个总顺序不是平台保证。BinderLab 分析器只检查上述稳定偏序,并接受 post 或 callback 早于 return 的合法交错。这条链能证明转线程和角色反转;oneway callback 不参与原同步事务嵌套,也不能证明等待线程复用。

实验 5:同步 callback

使用独立的非 oneway 接口:

interface ISyncResultCallback {
    int onResult(int requestId, int value);
}
​
interface ICalculator {
    int addAndCallback(
        int requestId,
        int a,
        int b,
        ISyncResultCallback callback);
}

服务端必须在 addAndCallback() 原同步事务内直接调用 callback.onResult(),不能先 post Handler,也不能复用异步 IResultCallback

App 线程 A1:C_SYNC_CALL_BEGIN(requestId=1001),调用 addAndCallback() 并同步等待
    ↓
服务端 Binder 线程 B1:S_BEFORE_SYNC_CALLBACK(requestId=1001)
    ↓
App 进程重新进入:C_SYNC_CALLBACK(requestId=1001)
    ↓
服务端:S_AFTER_SYNC_CALLBACK(requestId=1001)
    ↓
App 线程 A1:C_SYNC_CALL_END(requestId=1001)

先确认相同 requestId 的 callback 发生在外层同步调用尚未返回的嵌套区间内,再比较 C_SYNC_CALLBACK.tidC_SYNC_CALL_BEGIN.tid。只有“requestId 一致 + 严格嵌套顺序 + TID 相同”联合成立,才能证明该次运行复用了原等待线程。异步 callback 落在任意空闲 Binder 线程不能支持这个结论。完整实现见 BinderLab

API 36 实机证据分别覆盖了四组关系:Handler 对照显示 post() 自身很短,注入延迟集中在 H0B → H1结构化分析满足 blocked.queueNsMin - baseline.queueNsMax ~= injectedBlockerNs同/跨 node 对照显示两边第二笔均提前提交、node 内串行、不同 Binder TID 上存在重叠;同步重入满足同 requestId、严格嵌套和 TID 相同;async-callback把 Binder 入口、Handler 和反向 callback 分离到不同线程阶段。

这些日志可以排除发送过晚和单 Binder 线程等替代解释,但不是驱动 enqueue 的内部时间戳;一次调度结果也不保证不同 node 每次都并行。

线程答案必须带上调用模式

答案不能只写“Binder 线程”:

1. 跨进程事务先由服务端 Binder 线程或集成 Binder fd 的事件循环接收;
2. 普通 Java AIDL Stub.onTransact() 通常运行在服务端 Binder 线程;
3. 同步调用时客户端原线程等待 reply;
4. oneway 只取消业务 reply 等待,不创建线程,也不保证业务完成;
5. 同一 node 的 oneway 串行,但不固定线程;
6. 服务端可以显式 post 到 Handler,决定入口返回后再做,或让 Binder 线程同步等待;
7. 同步嵌套调用形成事务栈,调用链回到原进程时可能复用等待线程并产生重入;
8. 因此服务端必须同时考虑线程池并行、oneway 串行、Handler 排队和重入。

线程模型到这里闭合,但线程归属还不能回答调用身份。服务端入口虽然运行在自己的 Binder 线程上,当前入站事务仍携带发送方 PID/UID;一旦把工作转给 Handler,这份 calling context 又不会自动跟过去。第七篇从这条身份时间线继续。

源码与官方文档