驱动基石:Binder 驱动在内核中的核心作用与关键数据结构
聊到 Binder IPC,很多人第一反应是 Java 层的 ServiceManager、aidl 生成的 Stub/Proxy。但说实话,这些只是冰山上的装饰。真正让 Binder 跑起来的,是内核里的那个驱动——Binder Driver。
我个人习惯把 Binder 驱动比作「内核里的快递中转站」。用户空间的进程把数据包扔给它,它负责拆包、路由、投递。没有这个驱动,Binder 就是空中楼阁。今天我们就来拆开这个中转站,看看它到底长什么样。
Binder 驱动的核心定位
Binder 驱动不是一个标准的字符设备驱动,它更像一个专用的 IPC 路由器。它注册在 /dev/binder 节点上,用户空间通过 open()、ioctl()、mmap() 来跟它交互。
它的核心职责有三块:
- 进程间数据拷贝:一次拷贝,从发送方用户态到接收方内核态,再映射到接收方用户态。嗯,这就是 Binder 比传统 Socket 快的原因。
- 线程管理:维护每个进程的线程池状态,决定哪个线程来处理下一个事务。
- 引用计数与死亡通知:跟踪 Binder 对象的跨进程引用,当服务端挂了,通知客户端。
我在项目中遇到过一个问题:某个服务频繁重启,客户端一直收不到死亡通知,导致资源泄漏。查到最后发现是驱动层的引用计数没处理好。你想想看,这种问题在用户态根本复现不了,只能去读 /proc/binder/proc 下的状态。
关键数据结构:binder_proc
每个打开 /dev/binder 的进程,内核都会给它分配一个 binder_proc 结构体。这是进程维度的核心数据。
struct binder_proc {
struct hlist_node proc_node; // 挂在全局 proc 链表上
struct rb_root threads; // 红黑树,管理该进程的所有 binder 线程
struct rb_root nodes; // 红黑树,管理该进程导出的 binder 实体
struct rb_root refs_by_desc; // 按描述符索引的引用树
struct rb_root refs_by_node; // 按节点索引的引用树
int pid; // 进程 ID
struct vm_area_struct *vma; // mmap 映射的虚拟地址空间
struct task_struct *tsk; // 对应的 task 结构
// ... 还有一堆链表和锁
};
这个结构体里最让我印象深刻的是 vma 字段。Binder 驱动通过 mmap 在内核分配一块物理内存,然后映射到用户空间。发送数据时,驱动把数据从发送方的用户空间拷贝到这块内核缓存,再通过 vma 映射到接收方的用户空间。整个过程只有一次真正的内存拷贝。
核心要点:binder_proc 是每个进程的「身份证」,驱动通过它找到进程的线程池、导出的服务、持有的引用。
关键数据结构:binder_thread
每个参与 Binder 通信的线程,内核都会创建一个 binder_thread。它代表一个工作线程。
struct binder_thread {
struct binder_proc *proc; // 所属进程
struct rb_node rb_node; // 挂在 proc->threads 红黑树上
int pid; // 线程 ID
int looper; // 线程状态:等待、处理中、退出
struct binder_transaction *transaction_stack; // 当前正在处理的事务栈
struct list_head todo; // 待处理的事务链表
// ...
};
这里有个细节:transaction_stack 是一个栈结构。为什么是栈?因为 Binder 支持嵌套调用——A 调用 B,B 又调用 C,返回时得按后进先出的顺序。我曾经调试过一个死锁问题,就是某个线程的 transaction_stack 没清干净,导致后续事务一直卡住。
避坑指南:我曾经在分析 ANR 日志时,发现某个线程的 looper 状态一直是 BINDER_LOOPER_STATE_WAITING,但 todo 链表不为空。这说明驱动把事务投递过来了,但线程没有及时处理。这种问题通常是因为用户态的 Looper 没有正确唤醒。
关键数据结构:binder_node 与 binder_ref
这两个结构体是 Binder 对象跨进程传递的核心。
- binder_node:代表一个 Binder 实体(服务端导出的对象)。每个服务端进程有一个 node,内核维护它的强/弱引用计数。
- binder_ref:代表客户端对某个 Binder 实体的引用。每个客户端进程有一个 ref,里面保存了描述符(handle)。
说白了,node 是「服务端视角」,ref 是「客户端视角」。驱动通过红黑树把两者关联起来。当客户端通过 handle 调用服务端时,驱动先根据 handle 找到 ref,再根据 ref 找到 node,最后找到服务端进程。
// 客户端调用流程(简化)
handle -> binder_ref (在客户端进程的 refs_by_desc 树中查找)
-> binder_node (通过 ref->node 找到)
-> binder_proc (通过 node->proc 找到服务端进程)
-> binder_thread (从服务端线程池中选一个)
注意:binder_node 的引用计数分为强引用和弱引用。如果服务端进程异常退出,驱动会遍历所有引用该 node 的 ref,发送死亡通知。但如果你在客户端没有注册死亡通知回调,那就收不到。嗯,这就是我前面提到的那个坑。
关键数据结构:binder_transaction
每次 Binder 调用,驱动都会创建一个 binder_transaction 结构体。它记录了这次调用的完整上下文。
struct binder_transaction {
int debug_id;
struct binder_work work; // 挂载到 todo 链表的工作项
struct binder_thread *from; // 发起调用的线程
struct binder_transaction *from_parent; // 嵌套调用时的父事务
struct binder_proc *to_proc; // 目标进程
struct binder_thread *to_thread; // 目标线程
struct binder_buffer *buffer; // 数据缓冲区
unsigned int code; // 调用码(如 TRANSACTION_TYPE)
unsigned int flags;
// ...
};
这个结构体里,buffer 指向的是内核中那块 mmap 出来的缓存。数据就存在这里。驱动通过 copy_from_user 把发送方的数据拷进来,然后通过 copy_to_user 让接收方读走。但注意,接收方读的时候其实不需要拷贝——因为 mmap 已经映射好了,接收方直接访问自己的用户空间地址就能读到数据。
这就是 Binder 一次拷贝的真相。你想想看,传统 Socket 需要两次拷贝(发送方用户态→内核态→接收方用户态),Binder 省了一次。在移动设备上,这省下来的性能很可观。
SVG 图:Binder 驱动核心数据结构关系
驱动的工作流程:一次典型的 Binder 调用
光看数据结构有点干,我们串起来走一遍流程。假设进程 A 要调用进程 B 的服务:
- 准备阶段:进程 A 的线程调用 ioctl(BC_TRANSACTION),传入数据。驱动根据线程找到对应的 binder_thread,再找到 binder_proc。
- 查找目标:驱动从数据中提取 handle,在 binder_proc 的 refs_by_desc 树中找到 binder_ref,再通过 ref 找到 binder_node,最后找到进程 B 的 binder_proc。
- 分配缓存:驱动在进程 B 的 mmap 区域分配一块 binder_buffer,把数据从进程 A 的用户空间拷贝过来。
- 投递事务:驱动创建一个 binder_transaction,挂到进程 B 某个线程的 todo 链表上。如果所有线程都忙,就创建一个新的内核线程来处理。
- 唤醒接收:驱动唤醒进程 B 中等待的线程。线程从 todo 链表中取出事务,通过 mmap 映射直接读取数据。
- 返回结果:进程 B 处理完后,通过 ioctl(BC_REPLY) 返回结果。驱动重复上述过程,把结果送回进程 A。
整个过程,数据在内核里只拷贝了一次。这就是 Binder 高性能的底气。
总结一下:Binder 驱动是 IPC 的交通枢纽。binder_proc 管进程,binder_thread 管线程,binder_node/ref 管对象引用,binder_transaction 管数据传递。这四个结构体撑起了整个 Binder 通信的骨架。
嗯,说到这我想起一个细节。早期 Android 版本里,binder_proc 的线程池管理是用链表实现的,后来改成了红黑树。为什么?因为线程数量多了之后,链表查找太慢。红黑树能把查找时间降到 O(log n)。这种优化,你在用户态根本感知不到,但内核开发者一直在做。
所以,当你下次遇到 Binder 相关的性能问题,别急着怀疑应用层。先看看 /proc/binder/proc 里的线程数、事务队列长度。很多时候,问题就出在驱动层的数据结构上。
公众号:蓝海资料掘金营,微信deep3321