Binder 驱动:一次事务如何穿过内核
Binder 驱动:一次事务如何穿过内核
上一章(Binder 是什么)我们停在一次调用的全景:Proxy 打包、驱动投递、 Stub 分发。这一章深入第一层,回答:transact() 之后,请求到底怎么穿过内核?
transact() 不会直接跳进服务端方法。客户端先通过 /dev/binder 的 ioctl 提交事务, 驱动找到目标进程与线程,将事务放入待处理队列;服务端线程从 ioctl 返回后,才开始执行 onTransact()。
一次拷贝不是零拷贝
| 常见误解 | 正确理解 |
|---|---|
| Binder 驱动像 socket,负责“传消息” | Binder 是字符设备 /dev/binder,通过 ioctl 交换命令,不是网络栈 |
| “一次拷贝”就是完全不拷贝 | 不是零拷贝:数据从客户端用户态拷入内核缓冲一次,服务端经 mmap 直接读,不再拷第二次 |
| 驱动会帮忙调用服务端代码 | 驱动只投递事务;服务端线程自己从 ioctl 返回并执行 onTransact() |
“一次拷贝”描述的是事务数据从客户端用户空间复制到内核后,服务端通过映射区域读取,省掉 第二次复制;它并不表示完全没有内存复制。驱动负责接收、路由和投递事务,不执行业务代码。
进程以 BC_ 命令向驱动提交操作,驱动以 BR_ 命令向进程返回事务、回复或状态变化。
核心结论
- 一次跨进程事务的主链是:客户端
transact()→ioctl(BC_TRANSACTION)→ 驱动校验并 投递 → 服务端ioctl返回BR_TRANSACTION→onTransact()执行 →BC_REPLY返回。 - 驱动在事务到达时校验调用方身份(UID/PID),并把它放入目标进程的待处理队列、唤醒服务端线程。
- mmap 让驱动只把数据从客户端拷入内核一次,服务端通过映射直接读取,这就是“一次拷贝”。
- 驱动不执行业务代码,也不创建线程;执行事务的是服务端进程自己的 Binder 线程。
主流程:一次事务如何穿过内核
逐步解释:
- 客户端调用
transact():调用线程进入IPCThreadState::transact(),把事务命令 写进与驱动的共享内存。 ioctl(BC_TRANSACTION)进入内核:驱动从binder_ioctl()开始处理。- 驱动校验并路由:读取事务里的目标 handle,校验发送方 UID/PID(并衔接 SELinux 的 binder 权限检查),找到目标进程的 Binder 节点。
- 投递与唤醒:驱动把事务挂到目标进程的待处理队列,必要时唤醒服务端的一个 Binder 线程。
- 服务端取件:服务端线程
ioctl返回BR_TRANSACTION,执行onTransact()。 - 回复:结果通过
BC_REPLY写回,驱动唤醒客户端调用线程,transact()返回。
为什么是“一次拷贝”
服务端进程初始化时会用 mmap 向驱动申请一块接收缓冲,驱动把这段缓冲同时映射到内核地址空间。 客户端发送时,驱动只需把数据从客户端用户态拷入这块缓冲一次;服务端线程读取时走的是 mmap 映射,不再产生第二次用户态拷贝。
因此“一次拷贝”对比的是 socket 的“用户态 → 内核态 → 内核态 → 用户态”两次拷贝。 它不是零拷贝,大块数据仍然有内核拷贝成本,这也是大文件用文件描述符传递的原因。
源码证据
阅读目标:确认客户端命令如何进入内核,以及服务端如何取回事务。
正文指针:IPCThreadState.cpp transact() → talkWithDriver();内核侧 binder.c binder_ioctl() 按命令分发的分支。
// IPCThreadState.cpp:客户端发送事务(伪代码,行号以 r75 为准)
writeTransactionData(...); // 组装 BC_TRANSACTION 命令
do {
talkWithDriver(); // ioctl 与驱动交换命令
} while (mOut.dataSize() > 0); // 直到待发送命令清空// binder.c:内核按命令分发的入口(伪代码,行号以 r75 为准)
static long binder_ioctl(...) {
switch (cmd) {
case BINDER_WRITE_READ:
// 读取 BC_ 命令、执行 binder_transaction() 等
break;
}
}这段代码证明: 客户端通过 ioctl 把命令批量交给驱动;binder_transaction() 是内核里 真正完成校验、拷贝和投递的函数。两端之间不存在“驱动主动调用服务端方法”的通道, 服务端线程必须自己从 ioctl 返回。
验证与排障
环境:已 root 的设备或模拟器,Binder 调试节点需要 root 权限;普通应用不可读。
adb shell su 0 cat /sys/kernel/debug/binder/transactions
adb shell su 0 cat /sys/kernel/debug/binder/proc/<pid>
adb shell dmesg | grep -i bindertransactions 节点列出内核中未完成的事务与等待线程;proc/<pid> 显示该进程已打开的 Binder 引用与缓冲。常见问题:事务超过缓冲上限(约 1MB)时驱动或上层会报错, 表现为 TransactionTooLargeException;进程持有大量 Binder 引用时,proc/<pid> 会很大。
这些节点是“内核视角”的证据,不能代替业务日志。先看事务是否在等待,再看服务端线程 是否执行,最后才怀疑业务实现。
常见误区
- “一次拷贝 = 零拷贝”:Binder 有且只有一次用户态 → 内核态的拷贝,不是完全没有拷贝。
- “驱动会调用服务端方法”:驱动只投递,执行者是服务端进程的 Binder 线程。
- “Binder 是消息队列”:Binder 是同步 RPC 模型,普通事务必须等 reply;
oneway是 显式声明的不等待模式,不能默认使用。 - “进程退出时 Binder 引用自动恢复”:驱动会清理节点并触发死亡通知,但客户端必须自己 处理重连(见死亡通知章节)。
延伸问题
- binderfs 和
/dev/binder的关系是什么?容器场景为什么需要 binderfs? - 驱动如何维护每个进程的待处理事务队列和等待线程集合?
- SELinux 的 binder 权限检查发生在驱动哪一步,和 UID/PID 校验有什么区别?
- “一次拷贝”在什么场景下会成为瓶颈?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
IPCThreadState.cpp | transact() / talkWithDriver() | 客户端组装并交换命令 |
IPCThreadState.cpp | waitForResponse() / executeCommand() | 等待 reply 与处理 BR_ 命令 |
binder.c | binder_ioctl() | 内核命令入口 |
binder.c | binder_transaction() | 校验、拷贝、投递事务 |
binder.c | binder_thread_read() | 服务端读取事务与唤醒逻辑 |
公共路径:Native 侧 frameworks/native/libs/binder/,内核侧 drivers/android/。 行号以 r75 检索为准。
事务如何组织数据与对象引用,见 Parcel 与 Transaction。