Binder(四):ioctl(BINDER_WRITE_READ) 之后,事务怎样到达目标进程?
第三篇已经建立了远端对象链:
text
BinderProxy
→ BpBinder(handle)
→ binder_ref
→ binder_node
→ 服务端本地 Binder 实体
现在客户端 IPCThreadState 已经持有:
text
目标 handle
transaction code
data Parcel
flags
并执行:
cpp
ioctl(fd, BINDER_WRITE_READ, &bwr)
驱动接下来必须完成的是:
Binder Driver 收到这些数据后,怎样解析目标、把事务准备到另一个进程、转换其中的 Binder 对象,并让服务端线程读到
BR_TRANSACTION?
调用栈在驱动中的主干如下:
text
IPCThreadState::transact()
↓ writeTransactionData()
BC_TRANSACTION + binder_transaction_data
↓ talkWithDriver()
ioctl(BINDER_WRITE_READ)
↓
binder_ioctl()
↓
binder_ioctl_write_read()
↓
binder_thread_write()
↓
binder_transaction()
↓
handle → binder_ref → binder_node → target_proc
↓
准备目标 Binder buffer
↓
复制普通数据 + 转换 Binder 对象 / fd
↓
进入明确目标线程或目标进程工作队列
↓
需要时唤醒相应 Binder 线程
↓
binder_thread_read()
↓
BR_TRANSACTION
驱动函数、字段和行号按导读第四节"系列统一基线"中的固定 commit 定位。本文跟踪 BC_TRANSACTION 到 BR_TRANSACTION 的正常主路径。
进入驱动前,先认清五类对象
五类对象足够描述一次路由
内核结构很多,但解释一次事务只需要先抓住五类:
| 内核对象 | 表示什么 | 与本次事务的关系 |
|---|---|---|
binder_proc |
一个打开 Binder Driver 的进程上下文 | 发送方和目标进程各有一个 |
binder_thread |
进程中一条与驱动交互的线程上下文 | 发送事务、等待 reply 或接收工作 |
binder_node |
服务端本地 Binder 实体的内核表示 | 决定目标端点及其拥有进程 |
binder_ref |
某进程对远端 node 的引用 | 把发送方 handle 解析到 node |
binder_transaction |
一笔正在传递或等待处理的事务 | 连接请求、目标工作和 reply |
可以先把关系压缩成:
text
发送方 binder_proc
└─ binder_thread
└─ binder_ref(handle)
└─ binder_node
└─ 目标 binder_proc
└─ 目标 binder_thread / proc work queue
handle 到目标进程的路由图

读图时只看四个阶段:
text
命令入口:BC_TRANSACTION
目标解析:handle → ref → node → proc
数据交付:buffer + 特殊对象转换 + work queue
用户态出口:BR_TRANSACTION / BR_REPLY
不要把驱动理解成"直接调用 Stub"。驱动交付的是事务和端点标识,服务端 libbinder 才会继续调用 BBinder、JavaBBinder 和 AIDL Stub。
用户态交给驱动的命令与数据
IPCThreadState::writeTransactionData() 把输出缓冲区组织成:
text
BC_TRANSACTION
+
binder_transaction_data
Android Common Kernel UAPI 的 binder_transaction_data 关键字段可以整理为:
c
struct binder_transaction_data {
union {
__u32 handle; // 发送请求时的远端 handle
binder_uintptr_t ptr;
} target;
binder_uintptr_t cookie;
__u32 code;
__u32 flags;
pid_t sender_pid;
uid_t sender_euid;
binder_size_t data_size;
binder_size_t offsets_size;
union {
struct {
binder_uintptr_t buffer;
binder_uintptr_t offsets;
} ptr;
} data;
};
发送请求时,用户态主要提供:
text
target.handle:目标远端引用
code:接口方法编号
flags:同步 / oneway 等事务标记
data_size:普通数据区大小
offsets_size:特殊对象位置表大小
buffer / offsets:发送方用户态 Parcel 数据
sender_pid 和 sender_euid 不是让发送方随意伪造后被服务端信任的普通业务字段。即使 UAPI struct 在发送侧也能看见同名存储,客户端写入的数值也不是接收端可信身份来源;驱动依据真实发送任务建立事务来源,并在交付接收侧时填入可信 calling context。第七篇再解释这个上下文怎样进入 IPCThreadState。
target 是 union,同一块存储在不同阶段有不同解释,不能把 handle、ptr 和 cookie 当成一笔事务里同时由客户端填写的三个目标字段:
| 阶段 | target 的解释 |
|---|---|
| 客户端向驱动发送普通事务 | target.handle:发送进程内的远端引用编号 |
| 驱动向目标进程返回事务 | target.ptr + cookie:让目标进程 libbinder 恢复本地端点 |
| reply | 根据原事务和 transaction_stack 定位等待线程,不再把它当普通目标 handle |
表中的 handle、binder_ref 和 binder_node 对象身份已由第三篇建立;本篇只回答 binder_transaction() 怎样实际使用它们。
BINDER_WRITE_READ 同时承载写命令和读结果
binder_write_read 同时携带:
c
struct binder_write_read {
binder_size_t write_size;
binder_size_t write_consumed;
binder_uintptr_t write_buffer;
binder_size_t read_size;
binder_size_t read_consumed;
binder_uintptr_t read_buffer;
};
它允许一次 ioctl 中同时表达:
text
用户态还有哪些 BC_* 命令要写给驱动;
用户态准备了多少空间接收驱动返回的 BR_* 命令。
因此 BINDER_WRITE_READ 不是"只写一笔事务"的系统调用。IPCThreadState::talkWithDriver() 可以批量提交输出命令,也可以在同一次驱动往返中读取事务、reply、引用通知、死亡通知或线程池命令。
这也是为什么命名是:
text
WRITE_READ
而不是:
text
SEND_TRANSACTION
ioctl 进入 binder_transaction() 的入口链
固定内核基线中的调用入口是:
text
binder_ioctl() # binder.c:6034
↓ BINDER_WRITE_READ
binder_ioctl_write_read() # binder.c:5697
↓ 有 write_size
binder_thread_write() # binder.c:4340
↓ 读到 BC_TRANSACTION
binder_transaction() # binder.c:3248
源码入口:
到 binder_transaction() 才进入本篇主问题:目标是谁、数据放哪里、工作交给谁。
handle 经 binder_ref 找到 node 与目标进程
非 reply 的普通事务先检查目标 handle。
1. 非零 handle
固定基线的 binder_transaction() 目标解析 主干是:
c
ref = binder_get_ref_olocked(
proc,
tr->target.handle,
true);
if (ref) {
target_node = binder_get_node_refs_for_txn(
ref->node,
&target_proc,
&return_error);
}
注意查找条件同时包含:
text
发送方 binder_proc
+
发送方提交的 handle
所以 handle 的进程内作用域在驱动代码中是实实在在的。
2. handle 0
tr->target.handle == 0 是上下文管理者的特殊入口:
c
target_node = context->binder_context_mgr_node;
这为 ServiceManager 的引导发现保留了一个大家预先约定的根端点。第五篇再展开 handle 0 怎样建立以及服务名怎样解析成普通 Binder 引用。
3. node 为什么能确定目标进程?
binder_node 保存:
text
node->proc
也就是拥有本地 Binder 实体的 binder_proc。驱动取得 node 后,就能得到目标进程并为其准备事务。
事务怎样变成目标进程的工作
为目标进程准备 Binder buffer
找到 target_proc 后,驱动不是把发送方用户指针直接交给服务端。
固定基线在 binder_transaction() 为目标进程分配 Binder buffer:
c
t->buffer = binder_alloc_new_buf(
&target_proc->alloc,
tr->data_size,
tr->offsets_size,
extra_buffers_size,
is_oneway);
然后把普通数据从发送方用户空间复制到目标进程的 Binder buffer:
c
binder_alloc_copy_user_to_buffer(
&target_proc->alloc,
t->buffer,
user_offset,
user_buffer + user_offset,
remaining_size);
服务端用户态稍后得到的是目标 buffer 在自己地址空间中的映射地址。
因此,不要用一句"Binder 零拷贝"概括整条路径。更稳妥的说法是:
驱动把发送方用户空间数据复制到目标进程可访问的 Binder buffer;接收侧通过映射访问这块目标 buffer,从而减少额外的接收侧复制,但事务仍然发生了发送方到目标 buffer 的数据复制。
普通字节复制与 Binder 对象转换是两件事
Parcel 中有两类内容:
text
普通数据:int、String、Parcelable 字段等;
特殊对象:Binder 引用、文件描述符、buffer 对象等。
普通数据可以按字节复制。特殊对象不能。
驱动根据 offsets 表逐个识别对象类型,并进入不同转换分支,例如:
text
binder_translate_binder()
binder_translate_handle()
binder_translate_fd()
其目标不是理解业务,而是维护跨进程语义:
text
本地 Binder 实体 → 接收进程自己的 binder_ref / handle
远端 handle → 解析原 node,再映射为接收进程自己的引用
文件描述符 → 在目标进程 fd 表中安装对应 fd
这解释了为什么 Parcel 还要提交对象 offsets:驱动必须知道 buffer 中哪些位置不能当普通字节透明复制。
把事务放进明确线程或进程工作队列
数据和对象转换完成后,驱动要先选择工作的下一存放位置。对于当前可以交付的事务,驱动需要:
- 交给明确目标线程或目标进程工作队列;
- 必要时唤醒目标 Binder 线程。
工作可能交给:
text
一条明确的目标 binder_thread
或
目标 binder_proc 的待处理工作队列
普通新事务通常进入目标 binder_proc 工作队列,由可用线程领取;嵌套事务、reply 或已经有明确目标线程的分支,也可以直接面向特定 binder_thread。
对于客户端发出的、需要 reply 的同步请求,固定基线的 !TF_ONE_WAY 分支 还会:
text
设置 need_reply;
通过 from_parent 接上发送线程原有的 transaction_stack;
把当前事务压入发送线程的 transaction_stack。
这条 transaction_stack 保存的是等待 reply 的同步调用关系,用于 reply 关联、嵌套事务和重入;它不是所有 Binder work 的通用队列。
对于 TF_ONE_WAY 请求,上述压栈分支不会执行。共享的 binder_proc_transaction() 会使用 node 的:
text
has_async_transaction
async_todo
维护同一 node 的异步事务串行化:当前没有异步事务占用该 node 时,这笔 oneway 可以进入目标线程 / 进程队列;已有一笔时,后续 oneway 先留在 async_todo,等待前一笔释放串行位置。两种情况都不建立等待 reply 所需的发送线程事务栈。其并发语义留到第六篇。
服务端从 read 路径收到 BR_TRANSACTION
目标 Binder 线程通过自己的 BINDER_WRITE_READ 读取工作。驱动在 binder_thread_read() 中把事务整理为接收侧 binder_transaction_data:
c
if (t->buffer->target_node) {
trd->target.ptr = target_node->ptr;
trd->cookie = target_node->cookie;
cmd = BR_TRANSACTION;
} else {
cmd = BR_REPLY;
}
trd->code = t->code;
trd->flags = t->flags;
trd->sender_euid = ...;
trd->data.ptr.buffer = t->buffer->user_data;
然后把:
text
BR_TRANSACTION
+
接收侧 transaction_data
复制到服务端线程的 read buffer。
服务端 IPCThreadState::executeCommand(BR_TRANSACTION) 才会:
text
用目标 buffer 建立 Parcel;
通过 target.ptr / cookie 找本地 BBinder;
调用 BBinder / JavaBBinder 的 transact 入口。
所以驱动交付到用户态的不是 Stub 对象,而是一份足以让本进程 libbinder 找到本地端点的事务描述。
同步 reply 沿 transaction stack 返回
同步事务不是两条互不相关的消息。
发送请求时,驱动把事务挂到客户端线程的事务栈;服务端处理时,事务也记录当前接收线程和上层事务关系。服务端发送 BC_REPLY 后,驱动通过 in_reply_to 找到正在等待的原事务和目标线程:
text
请求 T1
客户端 thread A 等待
↓
服务端 thread B 处理
↓ BC_REPLY(in_reply_to = T1)
驱动找到原客户端 thread A
↓
向 A 交付 BR_REPLY
这套事务栈还是嵌套 Binder 调用、线程复用和重入成立的基础。第六篇再把它放进线程模型。
用方向理解 BC_* 与 BR_*
名字不是"客户端命令"和"服务端命令",而是相对驱动方向:
text
BC_*:Binder Command
用户态写给驱动
BR_*:Binder Return
驱动返回给用户态
因此两边进程都可能发送 BC_TRANSACTION:
text
App 调 system_server:App 写 BC_TRANSACTION
system_server 回调 App:system_server 写 BC_TRANSACTION
两边也都可能收到 BR_TRANSACTION 或 BR_REPLY,取决于当前事务角色。
驱动能路由事务,却不理解业务方法
| 驱动能处理 | 驱动不理解 |
|---|---|
| 目标 handle / node | Java 类名和方法名 |
| transaction code / flags | code 为什么代表 add() |
| 数据 buffer 与 offsets | int 的业务含义 |
| Binder 对象和 fd | Parcelable 的领域模型 |
| sender UID / PID | 服务端业务权限规则 |
| 目标进程、线程和队列 | ActivityManager 的业务状态 |
边界如下:
text
AIDL Proxy / Stub 理解接口协议;
Binder Driver 理解对象引用、事务和路由。
驱动拒绝事务的几个位置
| 阶段 | 典型问题 | 上层可能看到 |
|---|---|---|
| 目标解析 | handle 无效 | BR_FAILED_REPLY / RemoteException |
| node/proc 生命周期 | 目标已死亡 | BR_DEAD_REPLY / DeadObjectException |
| buffer 分配 | 目标 buffer 空间不足 | failed transaction / 事务过大 |
| 用户数据复制 | 指针或 offsets 非法 | BR_FAILED_REPLY |
| 特殊对象转换 | Binder 对象或 fd 无法转换 | 事务失败 |
| reply 路由 | 等待链目标死亡 | dead reply |
这说明"驱动收到 ioctl"不等于"服务端方法已经开始执行"。至少还要经过目标解析、buffer 准备、对象转换和工作排队。
Perfetto 证据:区分"已发送"与"已执行"
在可调试设备上,可以录制包含 Binder Driver 和调度信息的 Perfetto trace:
powershell
adb shell perfetto `
-o /data/misc/perfetto-traces/binder.perfetto-trace `
-t 10s `
sched binder_driver
adb pull /data/misc/perfetto-traces/binder.perfetto-trace
设备版本、权限和可用 atrace 类别会影响命令;官方 Perfetto Android tracing 也推荐同时采集 sched 与 binder_driver,以便把事务和线程调度放在一条时间线上。
观察时不要只找一条 Binder slice,而要对齐:
text
客户端开始 transact 的时间;
驱动事务事件;
服务端 Binder 线程被唤醒和开始运行的时间;
服务端用户态方法 slice;
reply 返回客户端的时间。
这样才能区分:
text
驱动已经接收,但目标线程尚未执行;
目标线程已经执行,但服务端业务尚未返回;
服务端已经返回,但客户端尚未恢复运行。
BinderLab API 36 证据包只有 Java 业务 marker,没有 Perfetto Binder flow,因此不能用于声称已经测得驱动路由或排队时间。要区分发送、目标排队、服务端执行和 reply 恢复,需要把完整 Binder flow、调度事件和用户态埋点放到同一时间线;单条 Binder slice 不足以确定业务根因。
从 BC_TRANSACTION 到 BR_TRANSACTION
text
1. IPCThreadState 把 handle、code、flags 和 Parcel 组织成 BC_TRANSACTION;
2. BINDER_WRITE_READ 把用户态写命令和读缓冲一起交给驱动;
3. binder_transaction() 在发送方 binder_proc 中用 handle 查 binder_ref;
4. binder_ref 指向 binder_node,node 再确定 target_proc;
5. 驱动为目标进程准备 Binder buffer;
6. 普通数据复制到目标 buffer,Binder 对象和 fd 按接收方语义转换;
7. 当前可交付的 binder_transaction 进入明确目标线程或目标进程工作队列,并在需要时唤醒相应 Binder 线程;同一 node 后续 oneway 可能先等待在 async_todo;
8. 服务端 binder_thread_read() 把工作返回为 BR_TRANSACTION;
9. 同步 reply 再沿事务栈回到原客户端线程。
第 7 步中的目标线程 / 进程队列负责投递 Binder work;transaction_stack 只描述需要 reply 的同步调用关系,不能把两者合并成一条通用排队机制。第 9 步也只属于同步分支,oneway 不等待这条 reply。
这条路径解释了"已有 handle 时怎样投递事务"。系统刚启动或客户端第一次连接服务时,却还没有这个 handle;从名字取得首个 Binder 引用的问题,要回到 ServiceManager 的注册表和 handle 0。