Binder驱动层分析
说到Binder,我得先聊聊驱动层。很多开发者在应用层调调接口就觉得够了,但一旦遇到服务响应慢、进程间通信卡顿,你就会发现——不懂驱动层,根本查不到根因。我个人习惯先从驱动架构看起,心里有个全局图,后面调优才有底气。
驱动架构:一个字符设备的故事
Binder在Linux内核里注册为一个misc设备,主设备号10,次设备号动态分配。设备文件就是/dev/binder。嗯,这里要注意:不同Android版本可能还有/dev/hwbinder和/dev/vndbinder,但核心驱动逻辑是同一套。
驱动层主要干三件事:
- 管理进程上下文(binder_proc)
- 管理Binder实体(binder_node)
- 调度事务(binder_transaction)
我曾经遇到一个线上问题:系统服务GetService偶尔超时,抓trace发现Binder驱动里待处理事务队列堆积,最后查出来是某个多媒体服务频繁创建临时binder节点却不释放,导致驱动层哈希冲突严重。那次之后我专门复盘了驱动层的节点管理逻辑。
Binder节点与设备文件
每个打开/dev/binder的进程,内核都会创建一个binder_proc结构体。这个结构体里最重要的就是节点树和事务列表。节点又分为两类:
| 节点类型 | 说明 | 生命周期 |
|---|---|---|
| binder_node | 代表服务端暴露的一个Binder对象 | 由服务端创建,引用计数归零时销毁 |
| binder_ref | 客户端持有的该节点的本地句柄 | 客户端close fd或显式释放时销毁 |
驱动通过红黑树管理这些节点,查找速度是O(log n)。但如果你进程里注册了几万个binder对象(我在某个图库框架里见过),哈希冲突和树旋转都会带来性能损耗。
关键点:设备文件/dev/binder在用户空间表现为一个文件描述符。所有Binder通信都通过这个fd进行ioctl调用。说白了,Binder就是一个ioctl驱动的自定义IPC协议,没你想的那么神秘。
ioctl函数:通信的入口
驱动暴露的核心ioctl命令有三个:
BINDER_WRITE_READ // 读写事务数据(最常用)
BINDER_SET_CONTEXT_MGR // 设置context manager(ServiceManager专用)
BINDER_THREAD_EXIT // 通知驱动线程退出
其中BINDER_WRITE_READ承载了90%的Binder通信。调用时传入一个binder_write_read结构体:
struct binder_write_read {
signed long write_size; // 写缓冲区大小
signed long write_consumed;
unsigned long write_buffer;
signed long read_size; // 读缓冲区大小
signed long read_consumed;
unsigned long read_buffer;
};
驱动收到ioctl后,先处理写缓冲区(发出去的事务),再处理读缓冲区(接收的回复或通知)。你想想看,这个顺序意味着什么?如果你写的数据特别大,读缓冲区一直为空,驱动就会卡在write阶段。我曾经见过一个推送SDK,每次发送的消息体里带了整张图片的base64,结果主线程ioctl阻塞了几百毫秒。
调优提示:Binder事务大小建议控制在1MB以内,超过这个阈值驱动会使用单独的“大事务缓冲区”,性能会下降一个数量级。如果非要传大块数据,考虑用ashmem或文件描述符传递。
驱动的分层结构
下面这张图是我自己梳理的驱动层核心组件和调用链,你看一眼就能明白整个流程:
驱动层数据结构速查
我整理了一个驱动层最关键的几个结构体,你在分析btrace或者dumpsys时一定会遇到它们:
| 结构体 | 字段(部分) | 作用 |
|---|---|---|
| binder_proc | proc_node_tree, todo, threads | 管理进程内所有Binder线程、待处理事务 |
| binder_node | rb_node, proc, refs, internal_strong_refs | 代表一个Binder实体对象,挂在proc的树上 |
| binder_ref | node, desc, death | 客户端持有的句柄,包含死亡通知回调 |
| binder_transaction | work, buffer, from, to_proc | 一次Binder事务的数据载体 |
避坑指南:我曾经在优化一个内存泄漏问题时,发现某个服务进程的binder_node数量不断增长,但对应的binder_ref却被客户端提前close了。驱动侧node的强引用计数永远不会归零,因为ref被删除时没有正确解引用。最终我们通过在服务端统一管理node的生命周期,并注册死亡通知来兜底。
ioctl的执行流程
现在你大概能猜到一次典型的Binder调用在驱动层是怎么走的了:
- 客户端调用
ioctl(fd, BINDER_WRITE_READ, &bwr),进入内核。 - 驱动解析bwr中的写缓冲区,提取BC_TRANSACTION命令。
- 创建
binder_transaction对象,查找目标node对应的proc。 - 将事务挂到目标proc的
todo队列,并唤醒目标进程的Binder线程。 - 目标线程从读缓冲区取出BR_TRANSACTION数据,返回用户态。
- 回复时反向再走一遍流程。
这里有个容易被忽略的点:驱动在拷贝数据时会用到binder_alloc,它有自己的mmap区域,每个进程预映射了8KB的缓冲区(某些高版本可配置)。如果事务频繁且大小超过阈值,驱动会从更大的预分配池里切一块出来,这个过程可能会有锁竞争。
实战技巧:查看/proc/binder/proc和/sys/kernel/debug/binder下的文件,可以实时观测每个进程的binder事务量、等待队列长度、节点数。我在某次线上抖动时就是通过cat /d/binder/transaction_log定位到某个Service频繁发起空事务导致的wakeup storm。
还没结束,但先记住这些
驱动层的内容确实很底层,但你只要记住三点:
- 设备文件是入口,ioctl是通道
- 红黑树管理节点,todo队列调度事务
- 事务大小和频率决定了驱动性能瓶颈
我当年啃这部分源码时,最深的感触就是——Binder并没有用什么高深莫测的算法,它就是用最朴实的内核机制(等待队列、自旋锁、内存映射)实现了高性能的IPC。理解了驱动层,你才真正拿到了Binder优化的钥匙。