Skip to content

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/socket2 次(内核缓冲中转,收发各一次用户态拷贝)无调用语义、要自己组协议解包
共享内存(ashmem/dmabuf)理论 0 次同步全靠锁,对象生命周期无法管理,安全边界难做
Binder1 次单次事务有容量上限(见第八节)

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 分发到真实现 → reply

Java 侧线程模型: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); // 同进程直连!
    }
}

三个细节:

  1. DESCRIPTOR 是接口的全名字符串,onTransact 用 enforceInterface 防串线;事务码从 FIRST_CALL_TRANSACTION 起,前几个码被系统占用(PING/DUMP 等,以 AOSP 为准);
  2. 同进程优化:asInterface 若发现是本地对象直接返回 Stub,不走序列化——「Binder 一定跨进程」是错觉;
  3. 稳定接口(@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,以内核配置为准)。

  1. Bundle 携带超大列表/位图跨页面传 → Fragment Arguments 爆炸;
  2. Intent 携带大对象启动 Activity → 同样炸。正解:只传引用(fd/Uri/provider),大数据走 ContentProvider 或共享文件。

九、身份与安全:Binder 是权限体系的底座 ​

每个事务自带调用方真实 uid/pid 与 SELinux 上下文,服务端 checkCallingPermission、Binder.getCallingUid() 因此可信(本地 asInterface 调用除外,见上)。两个高频陷阱:

  • 身份混淆:Binder 线程上「当前身份」随调用切换,异步线程里再查就是错的——必须把 uid/pid 显式传下去;clearCallingIdentity 后忘记 restoreCallingIdentity 会让后续以 system 身份误操作;
  • dump 权限:dumpsys 经 Binder dump 码进入,普通进程只能 dump 自己;-a 全量需要特权,这就是很多字段「普通 adb 看不到」的原因;
  • 身份审计链:从 App 发起的请求,服务侧拿到 uid→映射 appId→再查权限/归属;任何「提权代理」类服务都要在自己的接口上重复鉴权,而不能信上游已经查过。

十、值得背下来的五个实战模式 ​

  1. 注册-监听 = 双向 Binder:客户端把回调 Stub 传给服务端,之后所有事件推送都是反向事务;注册了就要在生命周期终点反注册,否则服务端持着你的 Stub 强引用,内存泄漏与死亡风暴双杀;
  2. dumpsys 就是 Binder dump 码:ServiceManager.getService("window") + Binder.dump,每个服务实现 dump(FileDescriptor, PrintWriter, String[]),这是你能看懂任何服务的最低成本入口;
  3. Binder 线程上不干重活:收到请求就转投 handler 队列,重活交给专用线程——否则 16 线程被占满,整机陪你卡;这也是 system_server 内部大量 mHandler.post 的动机;
  4. Parcel 对象池:Message.obtain/Parcel.obtain 同源思想,高频路径自己 new Parcel 会被 GC 拖住,用 obtain/recycle;
  5. 死亡驱动的状态机:凡是跨进程长连接(客户端注册型服务),把对端死亡当成状态机的一等公民处理(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

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处