Appearance
04 Binder IPC:一次拷贝的跨进程血脉
本页导读:Android 里几乎所有「跨进程」最终都落到 Binder:startActivity 是 Binder,dumpsys 是 Binder,连 zygote 孵化请求虽然走 socket,但回执 attach 还是 Binder。本页回答四个问题:为什么是 Binder 而不是管道/共享内存;「一次拷贝」到底怎么做到;Proxy-Stub-ServiceManager 三件套在 Java/AIDL 里长什么样;以及那几条踩过才知道疼的坑(16 线程、约 1MB 事务缓冲、oneway 洪水、fd 泄漏)。基线 Android 16(API 36)。
一、为什么是 Binder:把三种老路走一遍
进程 A 把一份数据交给进程 B,经典路线各有各的坑:
| 方案 | 拷贝次数 | 致命短板 |
|---|---|---|
| pipe/socket | 2 次(内核缓冲中转,收发各一次用户态拷贝) | 无调用语义、要自己组协议解包 |
| 共享内存(ashmem/dmabuf) | 理论 0 次 | 同步全靠锁,对象生命周期无法管理,安全边界难做 |
| Binder | 1 次 | 单次事务有容量上限(见第八节) |
Binder 赢在把「传输 + 序列化 + 引用计数 + 身份鉴权 + 死亡通知」打包成内核机制:
- 一次拷贝:写入方
copy_from_user直达接收方已映射的缓冲区,接收方零拷贝直接读; - 句柄翻译:内核把 binder 对象引用翻译成接收进程的 handle,跨进程只传「代称」不传指针;
- 身份可信:事务携带真实 uid/pid(
getCallingUid不可伪造),权限检查才有地基(第 08 页权限体系); - 死亡通知:节点引用清零时内核广播 dead binder,
linkToDeath由此而来。
二、驱动视角:mmap 与 flat_binder_object
text
/dev/binder(app/system 用) /dev/hwbinder(HIDL 遗产)
/dev/vndbinder(vendor↔vendor) binderfs(Android 11+,可建 per-device)
内核源:GKI android common 下 binder.c(drivers 路径随内核版本,以 AOSP common 内核为准)
用户态:frameworks/native/libs/binder/(ProcessState.cpp / IPCThreadState.cpp / Parcel.cpp)进程 open + mmap binder 设备时,驱动按页分配「自身缓冲区」并把同一段物理页映射两次:一次给内核写路径、一次给用户态读路径。默认 mmap 区 1MB 减一页(早期版本仅 128K,约 P 时代提升,以 AOSP 为准)。
一次事务的数据体经 copy_from_user 写入目标进程缓冲区;Parcel 中扫描出的 flat_binder_object(BINDER_TYPE_BINDER/FD/HANDLE)由驱动改写偏移:fd 在目标进程 fd_install 出新号,binder 对象翻译成 handle。这就是「一次拷贝 + 内核代记账」的全部魔法。
补充三点工程细节:
- binderfs(Android 11+):除传统
/dev/binder外可在 binderfs 上按需创建独立设备,服务间隔离更细;framework 代码统一走 libbinder,无感切换; - 事务双向性:每个同步事务带 reply 缓冲,目标处理完沿原路写回;oneway 则没有 reply,纯单向;
- 节点与引用:内核用 binder_node(实体)/binder_ref(引用)两层结构维护引用计数,强引用归零时实体可被回收——这就是跨进程版 GC,也解释了为什么「不 unlink 回调会泄露对方进程」。
三、角色三件套:ServiceManager、Stub、Proxy
text
服务方进程 servicemanager 进程 调用方进程
Binder 对象(Stub) ──addService──▶ 名字→handle 表 ◀──getService── BinderProxy(handle)
▲ │
└──────────── 后续调用:handle→节点 直达,不再过 SM ────┘- servicemanager(
frameworks/native/cmds/servicemanager)是注册簿:ServiceManager.addService/getService走 handle 0 上下文管理者;它只做命名→句柄翻译,不在数据通路上; - Stub 是服务端骨架,Proxy 是客户端替身,
asInterface决定你拿到哪一面:
java
// frameworks/base/core/java/android/os/Binder.java + AIDL 生成物(示意)
public abstract class Binder implements IBinder { ... }
// AIDL 生成:interface { Stub extends ... implements I; Stub.Proxy implements I }
// Proxy.method(): Parcel.writeXXX → mRemote.transact(TRANSACTION_x) → 读 reply
// Stub.onTransact(): enforceInterface → 按 code 分发到真实现 → replyJava 侧线程模型:Binder.execTransact 由 IPCThreadState(C++)的线程池驱动——每个 Binder 线程就是一个 Binder:<pid>_N,这就是服务侧并发的来源(第八节讲上限)。
服务名与可见性:service list 能看到全部注册名;SELinux 的 service_manager 权限控制谁能 add/find 哪个名字,所以「dumpsys 某服务报 Permission denied」多半是 sepolicy 而非代码 bug;Android 11+ 部分服务可按调用方 uid 做注册表可见性隔离(以 AOSP 实现为准)。
四、AIDL 生成物长什么样
一个 IFoo.aidl 会展开出(以 AOSP 工具链生成代码为准):
java
// 生成物骨架(等价示意):Stub.asBinder、Proxy 各方法 transact、Default 空实现
static abstract class Stub extends Binder implements IFoo {
static IFoo asInterface(IBinder b) {
IInterface i = b.queryLocalInterface(DESCRIPTOR);
return (i instanceof IFoo) ? (IFoo) i : new Proxy(b); // 同进程直连!
}
}三个细节:
DESCRIPTOR是接口的全名字符串,onTransact用enforceInterface防串线;事务码从FIRST_CALL_TRANSACTION起,前几个码被系统占用(PING/DUMP 等,以 AOSP 为准);- 同进程优化:
asInterface若发现是本地对象直接返回 Stub,不走序列化——「Binder 一定跨进程」是错觉; - 稳定接口(
@VintfStability)是 HAL 专用标签,让 AIDL 跨系统版本二进制兼容(第 12 页),App 侧接口不需要。
五、oneway:不等回信的喊话
oneway 方法只发不等回:
- 调用方把数据交给驱动立即返回;接收方仍要排队、由 Binder 线程执行——oneway 不是异步回调,是"发出即忘";
- 没有 reply,异常与结果全被吞;对同一节点的大量 oneway 会积压,内核给 oneway 的缓冲配额有上限(默认约 1MB 量级,内核可配,以 AOSP/内核配置为准),打满报
FAILED BINDER TRANSACTION/AsyncProxy 异常; - 典型用户:Activity 生命周期回调
ActivityThread里很多项、Choreographer 帧回调。
实践提示:oneway 适合「通知型」语义(状态变了),不适合「指令型」语义(你必须处理完);后者用同步调用 + 服务端自己转线程。
六、死亡通知与 fd 传递
linkToDeath:客户端对 Proxy linkToDeath(recipient, flags);服务端进程死亡时节点(binder_node)被标记 dead,内核向所有登记方投递 BR_DEAD_BINDER/DEATH_RECIPIENT,Java 回调 binderDied。AMS 的 attach 链路、MediaSession、任何长连接服务都靠它感知对端猝死。unlink 与弱引用细节以 AOSP Binder.java 注释为准。
java
// 用法骨架(示意)
IBinder remote = …;
IBinder.DeathRecipient dr = () -> cleanupRemote(remote);
remote.linkToDeath(dr, 0); // 注册
// …生命周期终点…
remote.unlinkToDeath(dr, 0); // 反注册,否则自己成为泄漏- 死亡通知的强度:link 的是强引用(weak 模式会延迟到 GC 后);进程活着但 GC 掉了远端 Stub,DEATH 不会来——长生命周期监听务必持有强引用或用系统封装好的注册模板;
- fd 传递:
Parcel.writeFileDescriptor(fd)打包成BINDER_TYPE_FD,驱动在接收进程装出新的 fd 号指向同一 open file——这就是 Cursor(ContentProvider)、共享内存 fd、Socket fd 跨进程的方式。坑:fd 是进程级资源,用完必须两边各自 close,否则就是 fd 泄漏与Too many open files的经典事故现场;fd 泄漏也拖住 binder 节点。
七、AIDL 高级货:稳定性、回调、错误
- 稳定性(Stability):普通接口默认最弱档,HAL 用
@VintfStability提升到跨版本兼容档;vendor 通过 vndbinder 与 system 通信必须稳定接口; - 回调对象:把 Stub 当参数传(客户端是服务端),形成反向调用——ServiceManager 之外所有「注册监听」模式的共同骨架;注意回调方进程死亡要 unlinkToDeath 清理;
- 错误处理:
Parcel.enforceInterface失败抛 SecurityException 类异常;transact返回 false 通常是对方 Binder 死亡或参数超容量,排查从dmesg的 binder 日志与service list开始; - Ndk 后端:C++ 服务用
--lang=ndk生成 libbinder_ndk 绑定,语义与 Java 后端同构。
八、两条硬边界:16 线程与约 1MB
线程池:ProcessState::startThreadPool 默认给每进程 16 个 Binder 线程(含触发者;system_server 等关键进程会被抬高,具体值以 AOSP 为准)。为什么是 16:早期经验值,够吃突发又不至于让锁竞争失控;它不是正确性参数,只是背压旋钮。后果:
- 16 个线程被慢调用占满,第 17 个请求就排队——表现为「整机卡顿,logcat 却安静」;
- 同进程两个服务互相同步调用可能自锁(A 占满线程等 B,B 需要 A 的线程);跨进程死锁则是经典的锁序问题,
dumpsys binder//sys/kernel/debug/binder/stats能看事务积压(路径需 root,以内核配置为准)。
事务缓冲:单次 Binder 事务数据受 mmap 区约束(约 1MB 减头部,精确值以 AOSP 为准)。超了:Java 侧抛 TransactionTooLargeException。两个高频事故现场:
实践计量办法:对可疑进程 cat /proc/<pid>/status | grep VmPeak 不够,直接看 binder 侧——/sys/fs/binder/proc/<pid>/stats 与 dmesg 里的 bring size ... buffer size 日志能告诉你哪笔事务撞了墙(路径需 root,以内核配置为准)。
Bundle携带超大列表/位图跨页面传 → Fragment Arguments 爆炸;- Intent 携带大对象启动 Activity → 同样炸。正解:只传引用(fd/Uri/provider),大数据走 ContentProvider 或共享文件。
九、身份与安全:Binder 是权限体系的底座
每个事务自带调用方真实 uid/pid 与 SELinux 上下文,服务端 checkCallingPermission、Binder.getCallingUid() 因此可信(本地 asInterface 调用除外,见上)。两个高频陷阱:
- 身份混淆:Binder 线程上「当前身份」随调用切换,异步线程里再查就是错的——必须把 uid/pid 显式传下去;
clearCallingIdentity后忘记restoreCallingIdentity会让后续以 system 身份误操作; - dump 权限:
dumpsys经 Binderdump码进入,普通进程只能 dump 自己;-a全量需要特权,这就是很多字段「普通 adb 看不到」的原因; - 身份审计链:从 App 发起的请求,服务侧拿到 uid→映射 appId→再查权限/归属;任何「提权代理」类服务都要在自己的接口上重复鉴权,而不能信上游已经查过。
十、值得背下来的五个实战模式
- 注册-监听 = 双向 Binder:客户端把回调 Stub 传给服务端,之后所有事件推送都是反向事务;注册了就要在生命周期终点反注册,否则服务端持着你的 Stub 强引用,内存泄漏与死亡风暴双杀;
- dumpsys 就是 Binder dump 码:
ServiceManager.getService("window")+Binder.dump,每个服务实现dump(FileDescriptor, PrintWriter, String[]),这是你能看懂任何服务的最低成本入口; - Binder 线程上不干重活:收到请求就转投 handler 队列,重活交给专用线程——否则 16 线程被占满,整机陪你卡;这也是 system_server 内部大量
mHandler.post的动机; - Parcel 对象池:
Message.obtain/Parcel.obtain同源思想,高频路径自己 new Parcel 会被 GC 拖住,用 obtain/recycle; - 死亡驱动的状态机:凡是跨进程长连接(客户端注册型服务),把对端死亡当成状态机的一等公民处理(binderDied → 清理→可选重启),而不是 try/catch 里打一行日志。
十一、排障手册
| 症状 | 定位思路 |
|---|---|
| TransactionTooLargeException | 检查 Bundle/Intent 载荷;dumpsys 看 binder 事务 stats 的 failed_transaction_size |
| oneway 调用无反应无异常 | 接收端 Binder 线程池打满或对端进程死亡;linkToDeath 没接就永远不知道 |
| 跨进程调用稳定超时 | 服务端持锁阻塞:binder 线程都在等同一把锁;dump 栈看锁主 |
| fd 泄漏 | ls /proc/<pid>/fd 行数跟踪增长;重点查 Cursor/ParcelFileDescriptor 未 close |
| 死锁:两个进程互等 | 双端线程栈对照;把同步互调改成一侧 oneway 或加超时(shell service call 支持 t= 超时,以 toybox 为准) |
十二、版本注记
- Android 5~8(API 21~27):Binder 成为全系统唯一 IPC 主干;HIDL 在 8.0 登场但仍以 Binder 为传输层。
- Android 9/10(API 28~29):mmap 区升至约 1MB(早期 128K);AIDL 后端 NDK 稳定化;
ServiceManager侧出现 per-app 可见性。 - Android 10(API 29)补充:
IServiceManager的 AIDL 化让 servicemanager 本身也走稳定接口,为后续 binderfs 与 AIDL HAL 统一铺路。 - Android 11(API 30):binderfs 启用,可建 per-process binder 设备(如 netd/installd 场景);oneway 背压机制进内核(具体以内核版本为准)。
- Android 12(API 31):
BinderTransaction可观测性增强(binder trace 可进 perfetto); - Android 13~14(API 33~34):libbinder 对 Parcel 校验与 fd 生命周期继续收紧;ANR 归因中 binder 堆栈采集更完整。
- Android 15~16(API 35~36):libbinder 持续加固(fd 校验、firewall 默认化);HIDL 通道基本完成历史使命(第 12 页)。
本页小结
本页小结:Binder = 一次拷贝的共享缓冲 + 内核句柄翻译 + 可信身份 + 死亡广播。记住三句:同进程 asInterface 不走 Binder;oneway 只是没回信,不是不占资源;16 线程与约 1MB 是两条会真实撞上的墙。看到「无异常地卡住」,先想 Binder 线程池;看到「大数据爆炸」,先想事务缓冲。
参考
- Binder 官方指南:developer.android.com/guide/components/binder(Android 开发者文档 Binder 章;AIDL 见 developer.android.com/docs/framework/binder/aidl)
- libbinder 源码:cs.android.com/android/platform/frameworks/native/+/main/libs/binder
- servicemanager 源码:cs.android.com/android/platform/frameworks/native/+/main/cmds/servicemanager
- AIDL 参考(AOSP):source.android.com/docs/core/architecture/aidl
- Parcel 源码:cs.android.com/android/platform/frameworks/base/+/main/core/java/android/os/Parcel.java