驱动基石: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(客户端) binder_proc binder_thread binder_ref handle=1 → node binder_transaction 进程 B(服务端) binder_proc binder_thread binder_node 服务端实体对象 binder_buffer mmap 缓存区 内核空间(Kernel Space) ref→node 关联 事务传递(一次拷贝) mmap 映射

驱动的工作流程:一次典型的 Binder 调用

光看数据结构有点干,我们串起来走一遍流程。假设进程 A 要调用进程 B 的服务:

  1. 准备阶段:进程 A 的线程调用 ioctl(BC_TRANSACTION),传入数据。驱动根据线程找到对应的 binder_thread,再找到 binder_proc。
  2. 查找目标:驱动从数据中提取 handle,在 binder_proc 的 refs_by_desc 树中找到 binder_ref,再通过 ref 找到 binder_node,最后找到进程 B 的 binder_proc。
  3. 分配缓存:驱动在进程 B 的 mmap 区域分配一块 binder_buffer,把数据从进程 A 的用户空间拷贝过来。
  4. 投递事务:驱动创建一个 binder_transaction,挂到进程 B 某个线程的 todo 链表上。如果所有线程都忙,就创建一个新的内核线程来处理。
  5. 唤醒接收:驱动唤醒进程 B 中等待的线程。线程从 todo 链表中取出事务,通过 mmap 映射直接读取数据。
  6. 返回结果:进程 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