5. Binder Driver 内核模块:核心数据结构
好,咱们今天来聊聊 Binder 驱动里最核心的几个数据结构。说实话,这部分内容我当年啃源码的时候也绕了不少弯路。你想想看,一个驱动要管理那么多进程间的通信,没有一套精巧的数据结构撑着,根本跑不起来。
Binder 驱动里有四个关键结构体:binder_proc、binder_node、binder_ref 和 binder_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; // 标志位
// ...
};
注意 ptr 和 cookie 这两个字段。它们保存的是用户空间的地址。内核不会去解引用这些地址,只是把它们当作“令牌”传递。当 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 节点和引用,每个节点可以被多个引用指向,而事务则在进程间穿梭,连接着源和目标。
从这张图里你能看到,进程 A 的 binder_ref 指向进程 B 的 binder_node,而进程 B 的 binder_ref 指向进程 A 的 binder_node。这就是双向通信的基础。当进程 A 发起调用时,内核创建一个 binder_transaction,把它挂到进程 B 的 todo 队列里。进程 B 的线程从队列里取出事务,开始处理。
好了,这四个核心数据结构就讲到这里。它们之间的关系,说白了就是:进程通过引用找到节点,通过事务传递数据。理解了这个模型,Binder 驱动的工作原理你就掌握了一半。