Android binder学习笔记5 - binder transact内核态与用户态交互全链路解析
1. Binder跨进程通信的核心流程
Android系统的Binder机制是进程间通信(IPC)的基石,理解其内核态与用户态的交互全链路对深入掌握Android系统至关重要。让我们从一个实际场景出发:当客户端进程调用transact()方法发起跨进程请求时,数据究竟经历了怎样的旅程?
整个过程可以概括为三个阶段:用户空间数据准备、内核空间数据中转、目标进程数据接收。在这个过程中,binder_write_read结构体扮演着关键角色,它是用户态和内核态之间数据交换的载体。值得注意的是,整个流程中只有一次数据拷贝,这得益于Binder驱动精心设计的内存映射机制。
2. 用户空间的数据准备
2.1 IPCThreadState::transact的奥秘
当我们在客户端调用transact()方法时,实际上触发的是IPCThreadState::transact()的执行。这个方法做了三件关键事情:
- 设置事务标志(如TF_ACCEPT_FDS)
- 调用
writeTransactionData()准备事务数据 - 通过
waitForResponse()等待服务端响应
status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { flags |= TF_ACCEPT_FDS; err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, nullptr); if ((flags & TF_ONE_WAY) == 0) { if (reply) { err = waitForResponse(reply); } else { Parcel fakeReply; err = waitForResponse(&fakeReply); } } return err; }2.2 事务数据的封装艺术
writeTransactionData()方法的核心是构造binder_transaction_data结构体。这个结构体包含了目标服务的handle、事务代码、标志位以及实际要传输的数据。特别值得注意的是:
- 对于BpBinder使用handle标识目标服务
- 对于BBinder则使用ptr指针
- data_size和offsets_size分别表示数据大小和Binder对象偏移量
struct binder_transaction_data { union { __u32 handle; binder_uintptr_t ptr; } target; binder_uintptr_t cookie; __u32 code; __u32 flags; pid_t sender_pid; uid_t sender_euid; 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; };3. 内核空间的桥梁作用
3.1 与驱动交互的关键一步
当用户空间准备好数据后,通过talkWithDriver()方法与Binder驱动进行交互。这个方法构造了binder_write_read结构体,并通过ioctl系统调用进入内核空间:
status_t IPCThreadState::talkWithDriver(bool doReceive) { binder_write_read bwr; bwr.write_size = mOut.dataSize(); bwr.write_buffer = (uintptr_t)mOut.data(); bwr.read_size = mIn.dataCapacity(); bwr.read_buffer = (uintptr_t)mIn.data(); ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr); }3.2 内核中的事务处理流程
在内核空间,Binder驱动通过binder_ioctl_write_read()处理来自用户空间的请求。这个函数会根据bwr结构体中的信息决定是执行写操作、读操作还是两者都执行:
- 如果
write_size > 0,调用binder_thread_write() - 如果
read_size > 0,调用binder_thread_read()
static int binder_ioctl_write_read(struct file *filp, unsigned int cmd, unsigned long arg, struct binder_thread *thread) { if (bwr.write_size > 0) { ret = binder_thread_write(proc, thread, bwr.write_buffer, bwr.write_size, &bwr.write_consumed); } if (bwr.read_size > 0) { ret = binder_thread_read(proc, thread, bwr.read_buffer, bwr.read_size, &bwr.read_consumed, filp->f_flags & O_NONBLOCK); } }4. 数据在内核中的旅程
4.1 唯一的数据拷贝时刻
在binder_thread_write()中,当处理BC_TRANSACTION或BC_REPLY命令时,会通过copy_from_user()将用户空间的数据拷贝到内核空间。这是整个Binder通信过程中唯一的一次数据拷贝:
case BC_TRANSACTION: case BC_REPLY: { struct binder_transaction_data tr; if (copy_from_user(&tr, ptr, sizeof(tr))) return -EFAULT; binder_transaction(proc, thread, &tr, cmd == BC_REPLY, 0); break; }4.2 binder_transaction的复杂逻辑
binder_transaction()函数是Binder驱动中最复杂的部分之一,它主要完成以下工作:
- 根据handle找到目标进程和线程
- 在目标进程的空间中分配缓冲区
- 将事务数据拷贝到目标缓冲区
- 将事务加入目标线程的待处理队列
- 唤醒可能处于等待状态的目标线程
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) { // 根据handle找到目标进程 if (tr->target.handle) { ref = binder_get_ref_olocked(proc, tr->target.handle, true); target_node = binder_get_node_refs_for_txn(ref->node, &target_proc, &return_error); } else { // handle为0表示ServiceManager target_node = context->binder_context_mgr_node; target_node = binder_get_node_refs_for_txn(target_node, &target_proc, &return_error); } // 在目标进程分配缓冲区 t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size, tr->offsets_size, extra_buffers_size, !reply && (t->flags & TF_ONE_WAY), current->tgid); // 拷贝数据到目标缓冲区 binder_alloc_copy_user_to_buffer(&target_proc->alloc, t->buffer, 0, (const void __user *)(uintptr_t)tr->data.ptr.buffer, tr->data_size); // 将事务加入目标队列 if (!binder_proc_transaction(t, target_proc, target_thread)) goto err_dead_proc_or_thread; }5. 目标进程的响应处理
5.1 服务端如何接收请求
在服务端进程(如ServiceManager),通常会通过binder_loop()不断读取并处理来自Binder驱动的请求。当有新的请求到达时,binder_thread_read()会被调用来处理:
- 检查当前线程的待处理队列
- 从队列中取出工作项
- 根据工作项类型进行相应处理
- 将处理结果写回用户空间
static int binder_thread_read(struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, size_t size, binder_size_t *consumed, int non_block) { while (1) { if (!binder_worklist_empty_ilocked(&thread->todo)) list = &thread->todo; else if (!binder_worklist_empty_ilocked(&proc->todo) && wait_for_proc_work) list = &proc->todo; w = binder_dequeue_work_head_ilocked(list); switch (w->type) { case BINDER_WORK_TRANSACTION: { t = container_of(w, struct binder_transaction, work); if (t->buffer->target_node) { cmd = BR_TRANSACTION; } else { cmd = BR_REPLY; } // 准备返回给用户空间的数据 tr.target.ptr = target_node->ptr; tr.cookie = target_node->cookie; tr.code = t->code; tr.flags = t->flags; tr.data_size = t->buffer->data_size; tr.offsets_size = t->buffer->offsets_size; tr.data.ptr.buffer = (binder_uintptr_t)((uintptr_t)t->buffer->data + binder_alloc_get_user_buffer_offset(&proc->alloc)); tr.data.ptr.offsets = tr.data.ptr.buffer + ALIGN(t->buffer->data_size, sizeof(void *)); // 将数据拷贝到用户空间 if (put_user(cmd, (uint32_t __user *)ptr)) return -EFAULT; ptr += sizeof(uint32_t); if (copy_to_user(ptr, &tr, sizeof(tr))) return -EFAULT; ptr += sizeof(tr); break; } } } }5.2 同步与异步处理的差异
Binder事务可以分为同步和异步两种模式,它们在处理上有重要区别:
同步事务(非TF_ONE_WAY):
- 客户端会等待服务端响应
- 事务会被加入目标线程的事务栈
- 需要等待BR_REPLY才能完成
异步事务(TF_ONE_WAY):
- 客户端不等待响应
- 事务处理完毕后立即释放资源
- 适合不需要返回结果的操作
if (cmd == BR_TRANSACTION && !(t->flags & TF_ONE_WAY)) { // 同步事务处理 binder_inner_proc_lock(thread->proc); t->to_parent = thread->transaction_stack; t->to_thread = thread; thread->transaction_stack = t; binder_inner_proc_unlock(thread->proc); } else { // 异步事务处理 binder_free_transaction(t); }6. 完整交互链路的全景视图
通过上述分析,我们可以绘制出Binder跨进程调用的完整交互链路:
客户端用户空间:
- 构造请求数据(Parcel)
- 调用transact()发起请求
- 通过ioctl进入内核
内核空间处理:
- 查找目标进程和线程
- 执行唯一的数据拷贝
- 将事务加入目标队列
- 唤醒目标线程
服务端用户空间:
- 从内核读取请求
- 处理请求并准备响应
- 通过ioctl返回结果
内核空间响应:
- 将响应数据传递给客户端
- 唤醒等待的客户端线程
客户端用户空间:
- 读取并处理响应
- 完成整个调用过程
7. 性能优化的关键点
在实际开发中,理解Binder的交互链路有助于我们优化应用性能。以下是几个关键优化点:
减少传输数据量:
- 精简Parcel中的数据
- 避免传输大对象
合理使用同步/异步模式:
- 不需要返回结果时使用TF_ONE_WAY
- 同步调用注意超时处理
复用Binder连接:
- 避免频繁建立和断开连接
- 使用连接池管理长期服务
注意线程模型:
- 服务端使用多线程处理并发请求
- 避免在主线程进行耗时Binder调用
在实际项目中,我曾遇到一个性能问题:某个服务接口响应缓慢。通过分析发现,客户端频繁发起小数据量的同步调用,导致大量线程阻塞在Binder通信上。优化方案是将多个小请求合并为批量请求,并使用异步方式处理,性能提升了3倍以上。
