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或文件描述符传递。

驱动的分层结构

下面这张图是我自己梳理的驱动层核心组件和调用链,你看一眼就能明白整个流程:

Binder驱动层核心架构 用户空间 Client/Server 进程  |  ServiceManager  |  /dev/binder fd 内核层:Binder驱动 (ioctl) binder_ioctl() → BINDER_WRITE_READ / SET_CONTEXT_MGR / THREAD_EXIT binder_proc 进程上下文 / 线程池 / 待处理事务 binder_node / ref 红黑树 / 引用计数 / 死亡通知 binder_transaction 事务队列 / 工作项 / 缓冲区管理 硬件 / 内核调度器 & 内存 fork后子进程不会 继承binder fd状态 (需重新open)

驱动层数据结构速查

我整理了一个驱动层最关键的几个结构体,你在分析btrace或者dumpsys时一定会遇到它们:

结构体字段(部分)作用
binder_procproc_node_tree, todo, threads管理进程内所有Binder线程、待处理事务
binder_noderb_node, proc, refs, internal_strong_refs代表一个Binder实体对象,挂在proc的树上
binder_refnode, desc, death客户端持有的句柄,包含死亡通知回调
binder_transactionwork, buffer, from, to_proc一次Binder事务的数据载体

避坑指南:我曾经在优化一个内存泄漏问题时,发现某个服务进程的binder_node数量不断增长,但对应的binder_ref却被客户端提前close了。驱动侧node的强引用计数永远不会归零,因为ref被删除时没有正确解引用。最终我们通过在服务端统一管理node的生命周期,并注册死亡通知来兜底。

ioctl的执行流程

现在你大概能猜到一次典型的Binder调用在驱动层是怎么走的了:

  1. 客户端调用ioctl(fd, BINDER_WRITE_READ, &bwr),进入内核。
  2. 驱动解析bwr中的写缓冲区,提取BC_TRANSACTION命令。
  3. 创建binder_transaction对象,查找目标node对应的proc。
  4. 将事务挂到目标proc的todo队列,并唤醒目标进程的Binder线程。
  5. 目标线程从读缓冲区取出BR_TRANSACTION数据,返回用户态。
  6. 回复时反向再走一遍流程。

这里有个容易被忽略的点:驱动在拷贝数据时会用到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优化的钥匙。