Binder 不只是 IPC:从一次跨进程调用看懂 Android 与 AAOS 的架构演进
如果把 Android 系统看成一座城市,Binder 很像它的道路系统。
应用通过它寻找系统服务,Framework 通过它调用 native 服务,CarService 通过它访问 Vehicle HAL。看起来只是一次普通的方法调用,背后却可能跨过进程、用户、系统分区,甚至虚拟机边界。
很多 Binder 文章从驱动结构体开始讲,读到最后记住了一堆 binder_node、binder_ref,却仍然解释不了几个更重要的问题:Android 为什么没有只用 socket?Treble 为什么又引入 hwbinder 和 vndbinder?既然已经有 HIDL,为什么新 HAL 又回到了 AIDL?
这篇文章换一条线索:从 Binder 所解决的问题出发,沿着 Android 的系统架构演进,再回到一笔事务在源码中如何完成。最后结合 AAOS,看看这些变化如何影响车载系统的接口设计与故障处理。
本文讨论的现代实现以 Android 17 AOSP 发布源码为基线;驱动内部流程参考 Android 16、Linux 6.12 产品内核。Binder 的核心机制在两者之间是一致的,但具体设备上的内核补丁仍应以产品源码为准。
Binder 解决的从来不只是"传数据"
进程间传数据并不难。pipe、socket、共享内存都能做到,Binder 真正特别的地方,是它把 Android 系统需要的几种能力装进了同一个模型。
首先是调用者身份。
服务端处理 Binder 请求时,可以获得调用方的 UID 和 PID。权限判断因此发生在真实的服务入口,而不必相信客户端在消息里填写的身份。Binder 驱动还会在事务路由阶段进入 SELinux 安全检查,这让"谁能调用谁"成为系统强制访问控制的一部分。
其次是对象引用。
客户端拿到的不是服务对象本身,而是一个远端对象的代理。这个代理可以继续作为参数传给其他进程,也可以携带回调接口。驱动负责维护本地对象、远端 handle 和引用关系,进程退出时还能触发死亡通知。
最后才是传输效率。
Binder 在目标进程的映射区中分配事务 buffer,避免传统 IPC 路径中一次额外的数据复制。但"一次复制"并不等于"零成本":Parcel 仍要序列化,驱动仍要复制并修正其中的 Binder 对象和文件描述符。图片、音视频帧这类大数据,正确做法通常是传共享内存或 dma-buf 的 fd,而不是直接塞进 Parcel。
所以,Binder 更准确的定位不是"更快的 socket",而是一套带身份、对象能力和生命周期管理的本机 RPC 机制。
一次看似普通的方法调用,经历了什么
以客户端调用某个 AIDL 服务为例。应用代码看到的可能只是:
java
vehicleService.getProperty(propId, areaId);
跨进程后,这次调用大致会经过下面这条链路:
AIDL 生成的 Proxy 先把方法参数写入 Parcel,然后调用 transact()。native 层的 IPCThreadState 将事务编码为 BC_TRANSACTION,再通过 BINDER_WRITE_READ ioctl 交给驱动。
驱动收到事务后要完成几件关键工作:
- 根据客户端持有的 handle 找到
binder_ref,再定位服务端的binder_node和目标进程; - 校验 Binder 安全策略,决定调用是否允许;
- 在目标进程的 Binder 映射区分配 buffer,复制普通数据与对象偏移表;
- 把发送方语境中的 Binder 对象、handle 和 fd 转换成接收方可理解的形式;
- 将事务放进目标线程或目标进程的待处理队列,并唤醒 Binder 线程。
服务端线程拿到 BR_TRANSACTION 后,最终进入生成 Stub 的 onTransact()。同步调用处理完成后,再通过 BC_REPLY 和 BR_REPLY 沿原调用栈返回。
这里有两个容易忽略的事实。
第一,ServiceManager 不在每一次业务调用中转发数据。客户端第一次按名称查到服务后,后续事务直接发往目标 Binder 对象。ServiceManager 更像 DNS:负责注册和发现,不负责搬运每一个业务请求。
第二,Binder 线程池不是越大越好。线程太少会排队,线程太多则可能增加锁竞争和重入风险。尤其不要持有全局锁去同步回调一个不受控制的客户端,否则非常容易形成跨进程的 AB-BA 死锁。
Binder 如何变成 Android 的"系统总线"
只有对象引用还不够,客户端还需要知道到哪里寻找服务。
Binder 驱动只认识 node、handle 和 transaction,并不知道 activity、window 或 media.audio_flinger 是什么。ServiceManager 补上了这一层:服务端用字符串名称注册 Binder 对象,客户端再按名称查询。
于是 Android 形成了一套很稳定的服务模型:
text
服务端创建 Binder 对象
↓
向 ServiceManager 注册名称
↓
客户端按名称查询
↓
获得远端对象代理
↓
后续业务事务直接在两端之间传递
AIDL 又把这套模型中的手写协议变成了接口契约。开发者定义方法、参数和返回值,编译器生成 Proxy、Stub、事务码以及 Parcel 编解码代码。Java、C++、NDK 和 Rust 的入口不同,但底层面对的是同一套 Binder 对象与事务模型。
到这里,Binder 已经不只是一个驱动,而是由接口契约、对象模型、服务发现和传输机制共同组成的系统通信架构。
Treble 为什么要把一个 Binder 拆成三个域
Android 8 之前,Framework 与厂商实现之间缺少足够稳定的分区边界。系统升级时,Framework 的变化可能直接影响 vendor 中的 HAL;厂商内部类型和平台实现一旦耦合,OTA 就很难独立推进。
Project Treble 的目标,是让 system 与 vendor 可以按各自节奏演进。Binder 因而被划分为三个 context:
| Binder 域 | 设备节点 | 服务管理器 | 主要用途 |
|---|---|---|---|
| Framework Binder | /dev/binder |
servicemanager |
App、Framework、system native 服务 |
| Hardware Binder | /dev/hwbinder |
hwservicemanager |
binderized HIDL HAL |
| Vendor Binder | /dev/vndbinder |
vndservicemanager |
vendor 内部 Binder 服务 |
它们并不是三套不同的驱动算法。三者共享 Binder 驱动的事务、对象和 buffer 管理机制,但拥有独立的 context manager、handle 空间和服务命名空间。
这种隔离解决了边界问题,也引入了新的复杂度:服务需要选择正确的 Binder 域,跨域桥接变多,HIDL 与 AIDL 两套接口体系也需要同时维护。
下面这张图把 Treble 前、Treble/HIDL 阶段和 Stable AIDL 主线放在一起,更容易看清变化发生在哪里。

图 1:Binder 架构演进。Treble 先通过多个 Binder context 隔离运行域,现代 Android 再通过 Stable AIDL 治理跨分区接口。Stable AIDL 没有创造第四种 Binder 驱动。
需要特别说明的是,现代 Android 并没有删除 hwbinder 和 vndbinder。Android 17 的初始化配置仍会创建三个设备节点,并启动相应的服务管理器。变化在于:新 HAL 的主线方案已经从 HIDL 转向 Stable AIDL,存量架构则继续获得兼容支持。
Stable AIDL:真正稳定的是接口,不是设备节点
"新 HAL 回到 AIDL"很容易产生误解。它不是简单地把 .hal 文件换成 .aidl 文件,也不是让 vendor 随意使用普通 AIDL。
Stable AIDL 解决的是独立升级组件之间的兼容问题。一个接口发布后,会通过版本快照、哈希、冻结规则和 VINTF 声明受到约束。后续可以增加兼容内容,但不能让旧客户端或旧服务端突然无法通信。
典型的接口定义会包含类似配置:
bp
aidl_interface {
name: "android.hardware.example",
stability: "vintf",
frozen: true,
versions_with_info: [
{ version: "1", imports: [] },
],
backend: {
ndk: { enabled: true },
},
}
这里需要区分两个经常混在一起的概念:
- Stable AIDL 规定接口怎样版本化、怎样保持序列化协议兼容;
/dev/binder、/dev/hwbinder或其他后端决定对象通过什么 transport 传输。
Stable AIDL 本身不会"自动选择"设备节点。对于配置在系统 Binder 域中的 VINTF Stable AIDL HAL,system 与 vendor 可以通过 /dev/binder 通信,并由 VINTF、构建规则和 SELinux 共同约束边界。
这也是为什么现代方案看起来像是"又回到了 /dev/binder",实际上却和 Treble 前完全不同。Treble 前的问题是缺乏稳定契约;现在即使双方共享一个 Binder context,接口也已经被显式版本化和冻结。
在 AAOS 中,这条路线已经大规模落地。Vehicle、EVS、Audio Control、Occupant Awareness、Remote Access 等车载 HAL 都可以在当前 AOSP 中看到 VINTF Stable AIDL 的实践。
对车载平台来说,这个变化很重要。车机生命周期长,SoC、BSP、Android 平台和上层应用的升级节奏往往不一致。只有把跨分区接口做成可验证的长期契约,平台升级才不必要求所有组件同时换代。
Binder RPC:对象模型开始越过本机驱动
过去我们习惯把 Binder 和 /dev/binder 画等号,但现在这个等式已经不再完整。
当前 libbinder 中,BpBinder::transact() 会根据代理对象类型选择两条路径:普通 Binder 进入 IPCThreadState 和内核驱动,RPC Binder 则交给 RpcSession。
Binder RPC 可以使用 Unix Domain Socket、vsock 和 Trusty 等传输方式。它保留 IBinder 的对象调用体验,却不要求通信两端位于同一个 Linux Binder 驱动域。
这对虚拟化场景尤其有意义。例如车载座舱可能同时运行 Android、仪表域或隔离的服务虚拟机,vsock 可以跨越 VM 边界,Binder RPC 则让两端继续复用熟悉的接口和对象模型。
不过,复用对象模型不等于复用全部运行语义。换了 transport 以后,调用者身份、认证方式、fd 传递能力、断链模型和性能特征都可能变化。Binder RPC 适合受控的虚拟化与可信环境边界,不是把所有本机服务透明暴露到网络上的魔法开关。
放到 AAOS 中,哪些问题最值得关注
理解演进历史不是为了背三个设备节点,而是为了在真实系统中做出正确设计。AAOS 的 Binder 使用有几个特别容易踩坑的地方。
1. 多用户、多屏和多音区必须进入接口模型
手机上常见的"当前用户"或"默认显示"假设,在车机上往往不成立。同一时刻可能存在驾驶员屏、副驾屏、后排屏,以及多个 occupant zone 和 audio zone。
Binder 只负责送达调用,不会自动保证业务作用域正确。接口应显式携带必要的 userId、displayId、occupantZoneId 或 audioZoneId,服务端再结合 calling UID 做权限校验,不能直接相信客户端传入的身份。
2. 服务注册不代表服务已经可用
addService() 成功只说明 Binder 对象已进入服务目录,并不代表 CAN、数据库、音频设备或 Vehicle HAL 已经 ready。
服务最好把"对象可发现"和"业务可用"分开建模,提供明确的 readiness 状态、超时和降级行为。客户端也不应该在 SystemServer 主线程或启动关键锁内无限等待服务,否则一个下游硬件问题就可能拖垮整条启动链。
3. 死亡通知只告诉你连接断了
linkToDeath() 可以通知远端进程已经退出,但它不会恢复任何业务状态。
可靠的重连通常至少包含四步:清理旧 callback、重新查询服务、重建会话、重放可幂等的状态。还要给连接增加"代际"概念,拒绝旧服务迟到的回调。否则在 CarService、Vehicle HAL 或音频服务重启后,很容易出现用户、Display 或音区状态串代。
4. oneway 不是无限容量的异步队列
oneway 只表示调用方不等待业务 reply,事务仍要占用驱动 buffer,并在服务端队列中等待处理。同一个 Binder node 上的异步事务还需要保持顺序。
如果高频车辆信号逐条通过 oneway 上报,服务端处理速度稍慢就会持续积压。更好的设计通常是合并事件、只发送最新值,或为高吞吐数据选择共享内存等更合适的通道。
5. Binder 慢,未必是驱动慢
一次 transact() 的端到端耗时,可能包含线程池排队、锁等待、磁盘 I/O、HAL 调用、硬件响应和同步回调。只看到调用耗时长,就把问题归因于 Binder 驱动,通常会走错方向。
排查时应把 Perfetto 中的 Binder flow 与服务端 slice 对齐,再检查线程池是否耗尽、锁是否竞争、下游 HAL 是否阻塞。Binder 很多时候只是把真正的慢点暴露了出来。
几个常见误区
vndbinder 是性能更高的 Binder?
不是。它的核心价值是提供独立的 vendor 服务命名空间和访问边界,底层仍使用同一套 Binder 驱动机制。
有 AIDL 就代表接口稳定?
也不是。普通 AIDL 可以跟随同一个发布单元变化。只有经过版本快照、冻结和 stability 规则治理的 Stable AIDL,才适合充当独立升级组件之间的长期契约。
oneway 调用一定不会阻塞?
调用方不等待业务返回,但写入驱动、buffer 压力和 CPU 调度仍可能带来等待,服务端也可能发生队列积压。
TransactionTooLargeException 只表示单次请求太大?
不一定。Binder 映射区是进程内多个在途事务共享的有界资源,请求、回复和并发事务都可能贡献压力。异常本身不能精确告诉你是哪一侧耗尽了空间。
Stable AIDL 出现后,hwbinder 和 vndbinder 就消失了?
没有。新 HAL 的推荐路线已经变化,但存量 HIDL、vendor 内部服务和产品兼容代码仍可能依赖它们。
回头看 Binder 的演进
Binder 的历史并不是"IPC 越来越快"的历史,而是 Android 不断重新划定系统边界的过程。
最初,它解决进程之间如何高效、安全地调用;ServiceManager 让这些对象可以按名称被发现;Treble 用多个 Binder context 隔离 Framework 与 Vendor;Stable AIDL 把长期兼容写进接口和构建规则;Binder RPC 又让同一套对象模型能够跨越虚拟机与可信环境边界。
这些机制分别回答不同问题:
- AIDL 定义双方说什么;
- Stable AIDL 约束这套语言如何长期兼容;
- ServiceManager 决定服务如何被发现;
- Binder context 决定服务位于哪个命名空间;
- kernel Binder 或 Binder RPC 决定事务怎样被传输;
- 业务服务自己负责断线后的状态恢复。
对 Framework 和 AAOS 开发者来说,真正值得记住的是这组边界,而不是某个 Android 版本里恰好存在多少个设备节点。
当接口契约、服务归属、传输方式和故障恢复都被分别想清楚时,Binder 才不再只是一行 transact(),而会成为一套可升级、可诊断,也真正适合长期演进的系统架构。
参考源码
frameworks/native/libs/binder/BpBinder.cppframeworks/native/libs/binder/IPCThreadState.cppframeworks/native/libs/binder/ProcessState.cppframeworks/native/libs/binder/Stability.cppframeworks/native/libs/binder/RpcSession.cppframeworks/native/cmds/servicemanager/ServiceManager.cppsystem/tools/aidl/drivers/android/binder.cdrivers/android/binder_alloc.cdrivers/android/binderfs.chardware/interfaces/automotive/vehicle/aidl/hardware/interfaces/automotive/evs/aidl/hardware/interfaces/automotive/audiocontrol/aidl/