11. Binder 协议:BC(从用户态发往内核)与 BR(从内核发往用户态)协议解析

Binder 通信的本质,说白了就是用户态和内核态之间的一场“对话”。

用户态进程说一句,内核得听懂;内核回一句,用户态也得明白。那怎么保证双方都能准确理解对方的意思呢?

这就得靠一套固定的“暗号”系统——也就是 Binder 协议。

这套协议分为两大部分:BC(Binder Command)和 BR(Binder Return)。

BC 是用户态发给内核的命令,BR 是内核返回给用户态的结果。嗯,这里要注意,它们不是一一对应的请求-响应关系,而是一种异步的消息流。

11.1 BC 协议:用户态说了算

BC 协议,全称是 Binder Command。用户态进程通过 ioctl() 系统调用,把 BC 命令打包发送给 Binder 驱动。

我个人习惯把 BC 命令看作是“指令信封”,信封上写着要干什么,里面装着具体的数据。

常见的 BC 命令有哪些?我列几个你一定会遇到的:

BC 命令 含义 我在项目中遇到的坑
BC_TRANSACTION 发起一次跨进程调用 数据量太大时,这个命令会触发 Binder 缓冲区溢出
BC_REPLY 回复一次跨进程调用 回复超时会导致对端一直阻塞等待
BC_ACQUIRE 增加 Binder 对象的引用计数 忘记调用会导致 Binder 对象被提前释放
BC_RELEASE 减少 Binder 对象的引用计数 多减一次直接 crash,我调了一整天
BC_FREE_BUFFER 释放内核分配的缓冲区 不释放的话,内核内存泄漏,系统会越来越卡
BC_ENTER_LOOPER 通知内核,当前线程进入 Binder 循环 主线程必须调用,否则收不到任何 BR 消息

每个 BC 命令都对应一个结构体。比如 BC_TRANSACTION 对应的是 binder_transaction_data,里面包含了目标句柄、代码、数据指针等信息。

struct binder_transaction_data {
    union {
        __u32 handle;      // 目标 Binder 实体的句柄
        binder_uintptr_t ptr; // 内核内部使用
    } target;
    binder_uintptr_t cookie; // 附加数据
    __u32 code;            // 方法编号
    __u32 flags;           // 标志位
    binder_size_t data_size;    // 数据大小
    binder_size_t offsets_size; // 对象偏移数组大小
    union {
        struct {
            binder_uintptr_t buffer; // 数据缓冲区指针
            binder_uintptr_t offsets; // 对象偏移数组指针
        } ptr;
        __u8 buf[8]; // 小数据直接内联
    } data;
};

你想想看,这个结构体设计得很巧妙。小数据直接塞在 buf[8] 里,省去了一次内存拷贝。大数据才用指针传递。这就是 Binder 性能优化的一个小细节。

我的经验: 调试 BC 命令时,可以用 strace 抓取 ioctl 调用。我曾经靠这个手段,定位到一个第三方库频繁发送 BC_TRANSACTION 导致系统卡顿的问题。

11.2 BR 协议:内核说了算

BR 协议,全称是 Binder Return。内核处理完 BC 命令后,会把结果以 BR 消息的形式返回给用户态。

用户态线程通过 ioctl() 的返回值拿到这些 BR 消息,然后逐一处理。

常见的 BR 命令:

BR 命令 含义 注意事项
BR_TRANSACTION 内核通知用户态:有人调用了你 用户态需要处理这个请求,然后回复
BR_REPLY 内核通知用户态:你的调用有结果了 对应之前发出的 BC_TRANSACTION
BR_ACQUIRE 内核要求用户态增加引用计数 必须响应,否则引用计数会乱
BR_RELEASE 内核要求用户态减少引用计数 减到 0 时,可以释放本地资源
BR_SPAWN_LOOPER 内核建议用户态创建新线程 线程池管理的关键信号
BR_DEAD_BINDER 通知用户态:某个 Binder 服务端挂了 收到后需要做清理和重连
BR_CLEAR_DEATH_NOTIFICATION 死亡通知已清除 确认不再监听某个 Binder 的死亡
BR_OK 操作成功 最简单的成功响应
BR_ERROR 操作失败 需要检查错误码

BR 消息的处理是在 Binder 驱动中完成的。用户态只需要在循环中读取这些消息,然后根据类型分发处理。

核心要点: BC 和 BR 是异步的。你发送一个 BC_TRANSACTION,不会立即收到 BR_REPLY。内核可能先给你发别的 BR 消息,比如 BR_ACQUIRE 或 BR_TRANSACTION。所以用户态必须有一个消息循环,按顺序处理所有 BR 消息。

11.3 BC 与 BR 的交互流程

为了让你更直观地理解,我画了一张流程图。这张图展示了典型的 Binder 调用过程:

Binder BC/BR 交互流程图 客户端 用户态 Binder 驱动 内核态 服务端 用户态 ① BC_TRANSACTION ② 内核处理 ③ BR_TRANSACTION ④ 服务端处理请求 ⑤ BC_REPLY ⑥ 内核处理 ⑦ BR_REPLY ⑧ 客户端收到回复 BC 命令(用户态→内核) BR 命令(内核→用户态) 内核处理

从这张图你可以看到,一次完整的 Binder 调用,经历了 8 个步骤:

  1. 客户端发送 BC_TRANSACTION 给内核
  2. 内核解析命令,找到目标服务端
  3. 内核发送 BR_TRANSACTION 给服务端
  4. 服务端处理请求
  5. 服务端发送 BC_REPLY 给内核
  6. 内核处理回复数据
  7. 内核发送 BR_REPLY 给客户端
  8. 客户端收到回复,继续执行
我曾经踩过的坑: 有一次我写了一个 Binder 服务,发现客户端调用后一直卡住。查了半天,原来是服务端在处理 BR_TRANSACTION 时,忘记调用 BC_REPLY 了。内核一直在等回复,客户端也在等结果,两边都死锁了。所以,收到 BR_TRANSACTION 必须回复 BC_REPLY,这是铁律。

11.4 协议数据格式

BC 和 BR 命令在 ioctl 中传递时,使用的是统一的格式。每个命令由一个 32 位的命令码和可变长度的数据组成。

// ioctl 调用原型
int ioctl(int fd, int cmd, unsigned long arg);

// 对于 Binder,cmd 固定为 BINDER_WRITE_READ
// arg 指向 struct binder_write_read 结构体

struct binder_write_read {
    signed long write_size;   // 要写入的 BC 命令数据大小
    signed long write_consumed; // 已消耗的写入数据
    binder_uintptr_t write_buffer; // BC 命令数据缓冲区
    signed long read_size;    // 要读取的 BR 命令数据大小
    signed long read_consumed; // 已消耗的读取数据
    binder_uintptr_t read_buffer; // BR 命令数据缓冲区
};

你想想看,这个设计很有意思。一次 ioctl 调用,既可以发送 BC 命令,也可以接收 BR 命令。内核会先处理 write_buffer 中的 BC 命令,然后把产生的 BR 命令填入 read_buffer。

每个 BC 或 BR 命令在缓冲区中的格式是:

// 命令头
uint32_t cmd;  // 命令码,如 BC_TRANSACTION
// 命令数据
// 不同命令的数据长度不同
// 对于 BC_TRANSACTION,后面跟着 struct binder_transaction_data
调试技巧: 我经常在 binder_write_read 结构体的 write_buffer 和 read_buffer 处打断点。通过观察命令码和数据,可以精确追踪每一次 Binder 通信。这个技巧帮我解决过好几个诡异的跨进程问题。

11.5 协议的生命周期管理

BC 和 BR 协议不仅仅是数据传输,还负责 Binder 对象的生命周期管理。

当一个 Binder 对象被传递给另一个进程时,内核会自动发送 BC_ACQUIREBR_ACQUIRE 来增加引用计数。当不再需要时,发送 BC_RELEASEBR_RELEASE 来减少引用计数。

这个过程是自动的,但如果你手动操作不当,就会出问题。

举个例子:

// 错误示例:手动增加引用计数后忘记减少
// 这会导致 Binder 对象永远不会被释放
binder_inc_ref(ref, 0, NULL);  // 手动增加
// ... 使用对象 ...
// 忘记调用 binder_dec_ref() 来减少
核心原则: BC_ACQUIRE 和 BC_RELEASE 必须成对出现。BR_ACQUIRE 和 BR_RELEASE 也必须成对出现。这是 Binder 协议的基本契约,违反它会导致内存泄漏或野指针。

嗯,关于 BC 和 BR 协议,今天就聊到这里。这套协议是 Binder 通信的基石,理解了它,你就掌握了 Binder 的“语言”。下次遇到跨进程问题,不妨从协议层面入手,看看是哪个命令出了问题。


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