3、Binder驱动入门:Binder驱动的核心数据结构

好,咱们今天来啃一块硬骨头——Binder驱动的核心数据结构。说实话,我刚接触Binder时,被这一堆procnoderef搞得晕头转向。后来调了一个月的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;                      // 标志位
    // ...
};

这里有个关键点:ptrcookie。它们存的是用户空间的地址。驱动不关心你具体是什么对象,它只负责把这两个值原封不动地传给客户端。我曾经踩过一个坑——服务端进程被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_refdesc值也可能不同。比如进程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 四个结构体的协作关系

这四个结构体不是孤立的。我给你捋一下典型的调用流程:

  1. 进程A打开/dev/binder,内核创建binder_proc_A
  2. 进程A注册服务,内核创建binder_node,挂在binder_proc_A.nodes
  3. 进程B调用getService(),内核在binder_proc_B里创建binder_ref,指向那个binder_node
  4. 进程B发起IPC调用,内核分配binder_buffer存放数据,通过binder_ref找到binder_node,再把数据送到binder_proc_Atodo队列

你看,一环扣一环。任何一个结构体出问题,整个通信链路就断了。所以调试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驱动的初始化流程——看看这些结构体是怎么被创建和串联起来的。