5. Binder Driver 内核模块:核心数据结构

好,咱们今天来聊聊 Binder 驱动里最核心的几个数据结构。说实话,这部分内容我当年啃源码的时候也绕了不少弯路。你想想看,一个驱动要管理那么多进程间的通信,没有一套精巧的数据结构撑着,根本跑不起来。

Binder 驱动里有四个关键结构体:binder_procbinder_nodebinder_refbinder_transaction。它们分别对应着进程、Binder 实体、Binder 引用和事务。嗯,咱们一个一个来看。

5.1 binder_proc:进程的“身份证”

每个使用 Binder 的进程,在内核里都有一个 binder_proc 结构体。它记录了进程的所有 Binder 相关信息。我个人习惯把它理解为进程的“Binder 上下文”。

struct binder_proc {
    struct hlist_node proc_node;      // 挂入全局 proc 链表
    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;            // 待处理事务队列
    // ... 省略其他字段
};

这里我特别想提一下 todo 队列。我在项目中遇到过一个问题:某个服务进程的 todo 队列积压了大量事务,导致其他进程的 Binder 调用超时。排查下来,原来是该进程的主线程被一个耗时操作阻塞了,没及时处理队列里的请求。

核心要点:每个进程的 todo 队列是 Binder 通信的“缓冲区”。如果这个队列一直增长,说明处理速度跟不上生产速度,系统离卡顿不远了。

5.2 binder_node:Binder 实体的“本体”

当一个服务端注册了一个 Binder 服务,内核就会创建一个 binder_node。它代表了一个 Binder 实体对象。说白了,它就是服务端那个 Binder 对象在内核里的影子。

struct binder_node {
    struct hlist_node dead_node;      // 死亡通知链表
    struct rb_node rb_node;           // 挂入 proc 的 nodes 红黑树
    struct binder_proc *proc;         // 所属进程
    struct list_head refs;            // 指向该节点的所有 binder_ref
    binder_uintptr_t ptr;             // 用户空间 Binder 对象地址
    binder_uintptr_t cookie;          // 用户空间附加数据
    __u32 flags;                      // 标志位
    // ...
};

注意 ptrcookie 这两个字段。它们保存的是用户空间的地址。内核不会去解引用这些地址,只是把它们当作“令牌”传递。当 Binder 驱动把事务交给目标进程时,会把这些地址原样传回去,用户空间代码再用它们找到对应的 Binder 对象。

避坑指南:我曾经调试过一个诡异的问题——服务端收到的 Binder 对象地址总是错的。查了半天,发现是服务端进程在注册 Binder 服务后,又把那个对象给释放了,但没通知内核。内核里的 binder_node 还指向一个已经失效的地址。所以,注册 Binder 服务后,千万别随便释放对象。

5.3 binder_ref:Binder 引用的“门牌号”

客户端要调用服务端的方法,得先拿到一个 Binder 引用。这个引用在内核里就是 binder_ref。它本质上是一个“门牌号”,告诉内核你想找哪个 Binder 实体。

struct binder_ref {
    struct rb_node rb_node_desc;      // 按描述符索引
    struct rb_node rb_node_node;      // 按节点索引
    struct binder_proc *proc;         // 所属进程
    struct binder_node *node;         // 指向的 binder_node
    __u32 desc;                       // 描述符(句柄)
    // ...
};

每个进程维护了两棵红黑树来管理 binder_ref:一棵按描述符(desc)索引,一棵按节点(node)索引。这样,无论是通过句柄查找,还是通过节点查找,都能快速定位。

字段 含义 我的一点理解
desc 句柄值,用户空间通过它引用 Binder 就像文件描述符,是一个整数
node 指向实际的 Binder 实体 内核通过它找到服务端
proc 该引用所属的进程 每个进程有自己的引用空间

注意:不同进程的 desc 值可能相同,但指向不同的 Binder 实体。所以,千万别把一个进程的句柄直接传给另一个进程用——那会出大乱子。

5.4 binder_transaction:通信的“信使”

最后,也是最核心的——binder_transaction。每次 Binder 调用,内核都会创建一个事务对象,负责在进程间传递数据。

struct binder_transaction {
    struct list_head list;            // 挂入 todo 队列
    struct binder_proc *to_proc;      // 目标进程
    struct binder_thread *to_thread;  // 目标线程
    struct binder_proc *from_proc;    // 源进程
    struct binder_thread *from_thread;// 源线程
    struct binder_transaction *from_parent; // 父事务(用于同步)
    struct binder_work work;          // 工作项类型
    // ...
};

事务的流转过程很有意思。源进程发起调用后,内核创建一个 binder_transaction,把它挂到目标进程或目标线程的 todo 队列里。目标进程的 Binder 线程从队列里取出事务,处理完后,再创建一个回复事务,挂回源进程的队列。

嗯,这里有个细节:from_parent 字段。它用于处理同步调用时的嵌套关系。比如 A 调用 B,B 又调用 C,那么 C 回复 B 的事务,其 from_parent 就指向 B 调用 C 的那个事务。这样,内核就能正确地把数据一层层传回去。

一句话总结:binder_proc 是进程的“身份”,binder_node 是服务的“本体”,binder_ref 是客户端的“门牌”,binder_transaction 是通信的“信使”。这四个结构体,撑起了整个 Binder 通信的骨架。

5.5 核心数据结构关系图

为了让你更直观地理解它们之间的关系,我画了一张图。你看,一个进程可以有多个 Binder 节点和引用,每个节点可以被多个引用指向,而事务则在进程间穿梭,连接着源和目标。

Binder 核心数据结构关系图 进程 A (binder_proc) binder_node ptr=0x1234, cookie=0x5678 binder_ref desc=1 → 指向进程B的节点 todo 队列 待处理事务列表 线程 (binder_thread) 进程 B (binder_proc) binder_node ptr=0x9ABC, cookie=0xDEF0 binder_ref desc=1 → 指向进程A的节点 todo 队列 待处理事务列表 线程 (binder_thread) binder_transaction from: 进程A → to: 进程B 挂入队列 binder_ref → binder_node binder_node binder_ref todo队列 binder_transaction

从这张图里你能看到,进程 A 的 binder_ref 指向进程 B 的 binder_node,而进程 B 的 binder_ref 指向进程 A 的 binder_node。这就是双向通信的基础。当进程 A 发起调用时,内核创建一个 binder_transaction,把它挂到进程 B 的 todo 队列里。进程 B 的线程从队列里取出事务,开始处理。

好了,这四个核心数据结构就讲到这里。它们之间的关系,说白了就是:进程通过引用找到节点,通过事务传递数据。理解了这个模型,Binder 驱动的工作原理你就掌握了一半。


公众号:蓝海资料掘金营,微信 deep3321