3、Binder驱动入门:Binder驱动的核心数据结构
好,咱们今天来啃一块硬骨头——Binder驱动的核心数据结构。说实话,我刚接触Binder时,被这一堆proc、node、ref搞得晕头转向。后来调了一个月的Binder死锁问题,才真正把这些结构体刻在脑子里。
你想想看,Binder驱动就像个快递中转站。每天有成千上万的包裹(IPC调用)要处理。没有一套高效的数据结构,系统早就崩了。今天我就带你看看,这个中转站的核心台账长什么样。
3.1 binder_proc:每个进程的“档案袋”
每个使用Binder的进程,内核里都对应一个binder_proc结构体。我习惯把它叫做“进程档案袋”。
核心作用:记录一个进程在Binder世界里的所有家当——它打开了哪些Binder文件、分配了哪些内存、有哪些待处理的交易。
struct binder_proc {
struct hlist_node proc_node; // 挂入全局binder_procs链表
struct rb_root threads; // 线程红黑树
struct rb_root nodes; // 本进程创建的binder_node
struct rb_root refs_by_desc; // 按描述符索引的binder_ref
struct rb_root refs_by_node; // 按节点索引的binder_ref
int pid; // 进程ID
struct list_head todo; // 待处理工作队列
// ... 还有几十个字段,先记住这些核心的
};
我在项目中遇到过一个问题:某个服务进程频繁重启,每次重启后Binder通信就乱套。查了半天,发现是binder_proc里的todo队列没清干净,旧的事务还在排队。嗯,这里要注意——进程死亡时,驱动必须清理它的所有待处理事务。
binder_proc里最让我印象深刻的是那几棵红黑树。为什么用红黑树?说白了,就是查找快。你想想,一个系统服务可能有几百个Binder节点,每次通信都要查,用链表遍历就太慢了。
3.2 binder_node:Binder服务的“身份证”
当一个服务端通过ServiceManager.addService()注册自己时,Binder驱动会创建一个binder_node。这就是服务的“身份证”。
struct binder_node {
struct hlist_node dead_node; // 死亡通知链表
struct rb_node rb_node; // 挂在proc->nodes红黑树上
struct binder_proc *proc; // 所属进程
struct list_head refs; // 指向该节点的所有引用
binder_uintptr_t ptr; // 用户空间的binder对象地址
binder_uintptr_t cookie; // 用户空间的附加数据
__u32 flags; // 标志位
// ...
};
这里有个关键点:ptr和cookie。它们存的是用户空间的地址。驱动不关心你具体是什么对象,它只负责把这两个值原封不动地传给客户端。我曾经踩过一个坑——服务端进程被kill后,binder_node变成了“僵尸节点”,但客户端还拿着它的引用。结果一调用就崩。后来我加了个死亡通知机制,才搞定。
避坑指南:我曾经在调试一个系统服务时,发现binder_node的refs链表越来越长。原来是有个客户端反复获取服务引用但不释放。记住:每次getService()都会增加一个binder_ref引用计数,用完了一定要release。
3.3 binder_ref:客户端的“借书证”
客户端要调用服务端的方法,不能直接操作binder_node。驱动会给每个客户端发一个binder_ref,相当于一张“借书证”。
struct binder_ref {
struct rb_node rb_node_desc; // 按描述符挂在proc->refs_by_desc
struct rb_node rb_node_node; // 按节点挂在proc->refs_by_node
struct binder_proc *proc; // 所属客户端进程
struct binder_node *node; // 指向的服务节点
__u32 desc; // 客户端看到的句柄值(整数)
int strong; // 强引用计数
int weak; // 弱引用计数
// ...
};
你注意到没有?desc是个整数。这就是你在Java层调用transact()时传的那个code参数吗?不是的!desc是驱动内部用的句柄,Java层的code是方法编号。这两个概念别搞混了。
我个人习惯把binder_ref理解成“快捷方式”。同一个binder_node可以被多个进程引用,每个进程都有自己的binder_ref,desc值也可能不同。比如进程A看到的服务句柄是1,进程B看到的可能是5。
| 数据结构 | 类比 | 生命周期 | 创建时机 |
|---|---|---|---|
| binder_proc | 档案袋 | 进程存在期间 | 进程打开/dev/binder时 |
| binder_node | 身份证 | 服务注册到销毁 | 服务端创建binder对象时 |
| binder_ref | 借书证 | 客户端获取到释放 | 客户端getService()时 |
| binder_buffer | 信封 | 一次交易期间 | 发起IPC调用时 |
3.4 binder_buffer:IPC数据的“信封”
最后说说binder_buffer。每次IPC调用,数据都要从用户空间拷贝到内核空间。这个拷贝用的内存,就是通过binder_buffer管理的。
struct binder_buffer {
struct list_head entry; // 挂在proc->buffers链表
struct rb_node rb_node; // 按地址排序的红黑树节点
unsigned free:1; // 是否空闲
unsigned allow_user_free:1; // 是否允许用户释放
struct binder_transaction *transaction; // 关联的交易
struct binder_node *target_node; // 目标节点
size_t data_size; // 数据大小
size_t offsets_size; // 偏移数组大小
// ...
};
Binder驱动在内核里维护了一个内存池。每个binder_proc在打开驱动时,会通过mmap()映射一块内存。这块内存被切成一个个binder_buffer,用来存放IPC数据。
重要提醒:Binder的“一次拷贝”特性,就是靠这个内存池实现的。发送方把数据拷贝到内核的binder_buffer,接收方通过mmap直接映射到用户空间,不需要第二次拷贝。这也是Binder比传统Socket快的原因之一。
我记得有一次,系统出现“out of binder buffer”的报错。查了半天,发现是某个服务发了超大尺寸的Binder数据,把内存池撑爆了。后来我们限制了单次Binder传输的最大数据量(默认1MB),问题就解决了。
3.5 四个结构体的协作关系
这四个结构体不是孤立的。我给你捋一下典型的调用流程:
- 进程A打开
/dev/binder,内核创建binder_proc_A - 进程A注册服务,内核创建
binder_node,挂在binder_proc_A.nodes上 - 进程B调用
getService(),内核在binder_proc_B里创建binder_ref,指向那个binder_node - 进程B发起IPC调用,内核分配
binder_buffer存放数据,通过binder_ref找到binder_node,再把数据送到binder_proc_A的todo队列
你看,一环扣一环。任何一个结构体出问题,整个通信链路就断了。所以调试Binder问题时,我通常会先检查这四个结构体的状态——用cat /d/binder/proc/*就能看到它们的信息。
调试技巧:在Android设备上执行adb shell cat /d/binder/state,可以查看所有binder_proc、binder_node、binder_ref的实时状态。我曾经靠这个命令定位了一个服务泄漏的问题——某个binder_node的refs数量异常增长,顺藤摸瓜找到了那个忘记release的客户端。
好了,Binder驱动的四大核心数据结构就讲到这里。说白了,它们就是Binder通信的基石。你把这些结构体之间的关系理清了,Binder驱动的大框架也就掌握了八成。下一节,咱们聊聊Binder驱动的初始化流程——看看这些结构体是怎么被创建和串联起来的。