14. Binder 事务:binder_transaction 数据结构与事务的构建过程
Binder 通信的核心,说白了就是一次「事务」。你想想看,客户端要调用服务端的一个方法,参数怎么传过去?返回值怎么拿回来?中间跨了进程,数据怎么打包?这一切,都围绕着一个叫 binder_transaction 的数据结构展开。
我个人习惯把 Binder 事务比作一个「快递包裹」。你要寄东西,得先找个箱子,填好寄件人和收件人,再把东西放进去封好。Binder 事务也是一样——binder_transaction 就是那个箱子,里面装着你要传递的数据和元信息。
binder_transaction 数据结构长什么样?
咱们直接看代码。这个结构体定义在 kernel/drivers/android/binder.c 里,我挑几个关键字段说说:
struct binder_transaction {
struct binder_work work; // 挂到待处理队列用
struct binder_thread *from; // 发起事务的线程
struct binder_process *to_proc; // 目标进程
struct binder_thread *to_thread; // 目标线程(可能为NULL)
struct binder_transaction *from_parent; // 嵌套事务的父事务
struct binder_transaction *to_parent; // 被嵌套事务的子事务
struct binder_buffer *buffer; // 数据缓冲区
unsigned int code; // 事务码(比如BR_TRANSACTION)
unsigned int flags; // 标志位
struct binder_priority priority; // 优先级
struct binder_priority saved_priority; // 保存的原始优先级
bool is_failure; // 是否失败
bool is_async; // 是否异步
int pid; // 发起进程PID
int uid; // 发起进程UID
ktime_t start_time; // 事务开始时间
struct list_head fd_fixups; // 文件描述符修复列表
};
嗯,这里要注意,from_parent 和 to_parent 这两个字段是用来处理「嵌套事务」的。我在项目中遇到过一种情况:服务端在处理一个请求时,又主动调用了另一个 Binder 服务,这就形成了嵌套。如果没有这两个指针,事务的上下文就串不起来了。
核心要点:每个 Binder 事务都对应一个 binder_transaction 实例,它贯穿了整个 IPC 的生命周期——从发起、传输、到处理完成。
事务的构建过程:从用户态到内核态
事务是怎么构建起来的?我带你走一遍流程。假设你在 Java 层调用了 transact():
- Java 层打包:
Parcel把参数序列化,写入mData和mObjects。 - JNI 桥接:调用
android_os_Binder_exec_transact(),把 Parcel 数据传到 Native 层。 - Native 层封装:
IPCThreadState::transact()构造binder_transaction_data结构体。 - ioctl 进入内核:通过
BINDER_WRITE_READ命令,把数据传给内核驱动。 - 内核构建事务:
binder_transaction()函数分配binder_transaction对象,填充各个字段。
说白了,第5步才是真正的「事务构建」。前面几步都是在准备原材料。
内核中的事务构建:binder_transaction() 函数
这个函数是 Binder 驱动的核心之一。我挑几个关键步骤说说:
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
struct binder_transaction *t;
struct binder_work *tcomplete;
struct binder_proc *target_proc;
struct binder_thread *target_thread = NULL;
struct binder_node *target_node = NULL;
// ... 省略部分代码
// 1. 分配事务对象
t = kzalloc(sizeof(*t), GFP_KERNEL);
if (!t) {
return_error = BR_FAILED_REPLY;
goto err;
}
// 2. 分配数据缓冲区
t->buffer = binder_alloc_new_buf(&target_proc->alloc,
tr->data_size, tr->offsets_size,
extra_buffers_size, !reply && !(t->flags & TF_ONE_WAY));
// ... 省略错误处理
// 3. 拷贝用户数据到内核缓冲区
copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)tr->data.ptr.buffer,
tr->data_size);
copy_from_user(binder_alloc_copy_user_to_buffer(...));
// 4. 处理Binder对象引用
for (off = 0; off < tr->offsets_size / sizeof(size_t); off++) {
struct flat_binder_object *fp;
fp = (struct flat_binder_object *)(t->buffer->data + offsets[off]);
switch (fp->hdr.type) {
case BINDER_TYPE_BINDER:
// 处理Binder实体
break;
case BINDER_TYPE_HANDLE:
// 处理Binder句柄
break;
case BINDER_TYPE_FD:
// 处理文件描述符
break;
}
}
// 5. 挂入目标队列
t->work.type = BINDER_WORK_TRANSACTION;
list_add_tail(&t->work.entry, &target_list);
tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE;
list_add_tail(&tcomplete->entry, &thread->todo);
}
这段代码我简化了,但核心逻辑都在。你看,第1步分配事务对象,第2步分配缓冲区,第3步拷贝数据,第4步处理对象引用,第5步挂入队列。每一步都环环相扣。
我曾经踩过一个坑:在拷贝数据时,如果 copy_from_user 失败,必须回滚之前分配的所有资源。否则就会造成内存泄漏。Binder 驱动里用了 goto err 的清理机制,这个设计很值得学习。
事务的两种类型:同步 vs 异步
Binder 事务分两种:同步(one-way = false)和异步(one-way = true)。区别在哪?
| 特性 | 同步事务 | 异步事务 |
|---|---|---|
| 调用方是否阻塞 | 是,等待回复 | 否,立即返回 |
| 是否需要回复 | 是,必须有 BR_REPLY | 否,不需要回复 |
| 缓冲区分配策略 | 分配在内核缓冲区 | 分配在内核缓冲区 |
| 典型场景 | 方法调用、获取属性 | 事件通知、日志上报 |
| 事务队列 | 挂入目标线程的 todo 队列 | 挂入目标进程的 todo 队列 |
你想想看,为什么异步事务要挂到进程队列而不是线程队列?因为异步事务不需要指定目标线程,哪个线程空闲就由哪个线程处理。这样可以提高并发度。
数据缓冲区:binder_buffer 的结构
事务的数据存在 binder_buffer 里。这个结构体管理着内核中的一块连续内存:
struct binder_buffer {
struct list_head entry; // 链表节点
struct rb_node rb_node; // 红黑树节点
unsigned int free:1; // 是否空闲
unsigned int allow_user_free:1; // 是否允许用户释放
unsigned int async_transaction:1; // 是否异步事务
unsigned int debug_id:1;
struct binder_transaction *transaction; // 所属事务
struct binder_node *target_node; // 目标Binder节点
size_t data_size; // 数据大小
size_t offsets_size; // 偏移数组大小
size_t extra_buffers_size; // 额外缓冲区大小
void *data; // 数据起始地址
};
这里有个细节:data 指向的是内核虚拟地址,用户态不能直接访问。所以数据拷贝必须通过 copy_from_user / copy_to_user 来完成。嗯,这也是 Binder 安全性的一个体现——用户态程序无法直接操作内核内存。
事务构建的完整流程图
下面这张图展示了从用户态发起调用到内核构建事务的完整流程:
事务构建中的关键细节
有几个细节我觉得值得单独拿出来说说:
- 缓冲区分配策略:同步事务的缓冲区会优先分配在目标线程的「空闲缓冲区列表」中,这样可以减少内存碎片。异步事务则从进程的共享缓冲区池中分配。
- 对象引用的处理:当数据中包含 Binder 对象或文件描述符时,内核会进行「对象迁移」。比如文件描述符,内核会在目标进程中复制一个 fd,并自动关闭源进程中的 fd(如果设置了
TF_ACCEPT_FDS)。 - 优先级继承:如果目标线程的优先级低于发起线程,Binder 会临时提升目标线程的优先级。这就是
saved_priority字段的作用——处理完事务后再恢复回去。
一个小技巧:在调试 Binder 事务时,可以查看 /sys/kernel/debug/binder/proc/ 下的文件。每个进程的 Binder 状态都暴露在这里,包括待处理的事务列表、缓冲区使用情况等。我经常用这个来排查事务堆积的问题。
事务构建的异常处理
事务构建过程中可能出各种问题。我总结了几种常见情况:
- 内存不足:分配
binder_transaction或binder_buffer失败,返回BR_FAILED_REPLY。 - 目标进程已死亡:在查找目标节点时发现进程已退出,返回
BR_DEAD_REPLY。 - 数据拷贝失败:
copy_from_user时发生缺页异常或地址非法,返回BR_FAILED_REPLY。 - 对象引用无效:数据中的 Binder 句柄或文件描述符在目标进程中无效,返回
BR_FAILED_REPLY。
我曾经遇到过一个问题:某个服务频繁 crash,导致客户端调用时总是收到 BR_DEAD_REPLY。排查后发现是服务端的内存泄漏导致进程被 LMK 杀掉了。嗯,从那以后我养成了一个习惯——在客户端代码里一定要处理 DEAD_BINDER 异常,做重连或降级处理。
好了,关于 binder_transaction 数据结构和事务构建过程,就讲这么多。这个结构体虽然看起来字段很多,但核心逻辑并不复杂——分配、填充、挂入队列。理解了它,你就掌握了 Binder IPC 的「快递包裹」是怎么打包和寄出的。