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 性能优化的一个小细节。
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 驱动中完成的。用户态只需要在循环中读取这些消息,然后根据类型分发处理。
11.3 BC 与 BR 的交互流程
为了让你更直观地理解,我画了一张流程图。这张图展示了典型的 Binder 调用过程:
从这张图你可以看到,一次完整的 Binder 调用,经历了 8 个步骤:
- 客户端发送
BC_TRANSACTION给内核 - 内核解析命令,找到目标服务端
- 内核发送
BR_TRANSACTION给服务端 - 服务端处理请求
- 服务端发送
BC_REPLY给内核 - 内核处理回复数据
- 内核发送
BR_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
11.5 协议的生命周期管理
BC 和 BR 协议不仅仅是数据传输,还负责 Binder 对象的生命周期管理。
当一个 Binder 对象被传递给另一个进程时,内核会自动发送 BC_ACQUIRE 和 BR_ACQUIRE 来增加引用计数。当不再需要时,发送 BC_RELEASE 和 BR_RELEASE 来减少引用计数。
这个过程是自动的,但如果你手动操作不当,就会出问题。
举个例子:
// 错误示例:手动增加引用计数后忘记减少
// 这会导致 Binder 对象永远不会被释放
binder_inc_ref(ref, 0, NULL); // 手动增加
// ... 使用对象 ...
// 忘记调用 binder_dec_ref() 来减少
嗯,关于 BC 和 BR 协议,今天就聊到这里。这套协议是 Binder 通信的基石,理解了它,你就掌握了 Binder 的“语言”。下次遇到跨进程问题,不妨从协议层面入手,看看是哪个命令出了问题。
公众号:蓝海资料掘金营,微信deep3321