27. 数据发送:ioctl() 中的 BINDER_WRITE_READ 命令处理流程
好,我们继续往下挖。前面几章我们把 Binder 驱动层的核心数据结构、线程管理、内存映射都聊了一遍。现在终于到了最关键的环节——数据到底是怎么发出去的?
你想想看,上层调用 transact(),最终都会落到驱动层的 ioctl() 系统调用。而 ioctl() 里最核心的命令就是 BINDER_WRITE_READ。说白了,整个 Binder IPC 的数据传输,就是靠这个命令撑起来的。
我个人习惯把 BINDER_WRITE_READ 的处理流程分成三个阶段:入口解析 → 数据写入 → 数据读取。今天我们就重点讲「写入」这一半,读取的部分下一章再展开。
27.1 入口:binder_ioctl() 的调度逻辑
用户空间通过 ioctl(fd, BINDER_WRITE_READ, &bwr) 进入内核。驱动层的入口函数是 binder_ioctl()。嗯,这里要注意,这个函数不光处理 BINDER_WRITE_READ,还处理其他命令,比如 BINDER_SET_CONTEXT_MGR、BINDER_THREAD_EXIT 等等。
它的核心逻辑其实很简单——根据 cmd 参数做 switch-case 分发。我截取关键部分给你看:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct binder_proc *proc = filp->private_data;
struct binder_thread *thread;
unsigned int size = _IOC_SIZE(cmd);
void __user *ubuf = (void __user *)arg;
// 获取或创建当前线程的 binder_thread
thread = binder_get_thread(proc);
if (thread == NULL) {
return -ENOMEM;
}
switch (cmd) {
case BINDER_WRITE_READ: {
struct binder_write_read bwr;
// 从用户空间拷贝 bwr 结构体
if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
ret = -EFAULT;
goto err;
}
// 核心处理
ret = binder_ioctl_write_read(filp, cmd, arg, proc, thread);
if (ret)
goto err;
// 将结果写回用户空间
if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
ret = -EFAULT;
goto err;
}
break;
}
// ... 其他命令
}
}
这里有个细节我特别想强调:binder_get_thread() 这个函数。它会在当前进程的线程红黑树里查找,如果找不到就新建一个。每个线程在驱动层都有一个 binder_thread 结构体,用来管理这个线程的待处理事务和等待状态。
核心要点:BINDER_WRITE_READ 的处理入口做了三件事——从用户空间拷贝数据、调用核心处理函数、将结果写回用户空间。任何一步出错都会导致整个 ioctl 返回错误码。
27.2 核心:binder_ioctl_write_read() 的读写决策
进入 binder_ioctl_write_read() 后,驱动会根据 bwr 结构体中的 write_size 和 read_size 来决定做什么。
binder_write_read 结构体长这样:
struct binder_write_read {
// 写入部分
binder_size_t write_size; // 要写入的数据大小
binder_size_t write_consumed; // 已消耗的写入数据
binder_uintptr_t write_buffer; // 用户空间写入缓冲区地址
// 读取部分
binder_size_t read_size; // 要读取的数据大小
binder_size_t read_consumed; // 已消耗的读取数据
binder_uintptr_t read_buffer; // 用户空间读取缓冲区地址
};
处理逻辑其实就四个分支:
- write_size > 0 且 read_size > 0:先写后读。这是最常见的场景,比如发起一次同步调用。
- write_size > 0 且 read_size == 0:只写不读。比如发送一个异步通知。
- write_size == 0 且 read_size > 0:只读不写。比如等待对方回复。
- 两者都为 0:直接返回,什么都不做。
我记得在 Android 4.4 时代,有个 bug 就是某些场景下 write_size 和 read_size 同时为 0,导致驱动层空转,白白浪费了一次系统调用。后来 Google 在 5.0 里加了个 early return 优化。
27.3 写入处理:binder_thread_write() 的指令解析
当 write_size > 0 时,驱动会调用 binder_thread_write()。这个函数会从用户空间的 write_buffer 中逐个解析 Binder 指令。
Binder 指令是一系列 BC_XXX 宏定义的操作码,每个操作码后面跟着对应的参数。常见的指令有:
| 指令 | 含义 | 参数 |
|---|---|---|
| BC_TRANSACTION | 发起一次事务(调用) | struct binder_transaction_data |
| BC_REPLY | 回复一次事务 | struct binder_transaction_data |
| BC_FREE_BUFFER | 释放内核缓冲区 | binder_uintptr_t |
| BC_INCREFS / BC_ACQUIRE | 增加 Binder 引用计数 | __u32 target |
| BC_RELEASE / BC_DECREFS | 减少 Binder 引用计数 | __u32 target |
| BC_ENTER_LOOPER / BC_EXIT_LOOPER | 线程循环状态管理 | 无 |
处理流程是一个 while 循环,每次读取一个操作码,然后根据操作码执行对应的处理函数。代码结构大致如下:
static int binder_thread_write(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
uint32_t cmd;
void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
void __user *ptr = buffer + *consumed;
void __user *end = buffer + size;
while (ptr < end && thread->return_error == BR_OK) {
// 读取操作码
if (get_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
switch (cmd) {
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
// 拷贝事务数据
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
// 核心:创建并发送事务
binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
break;
}
case BC_FREE_BUFFER: {
binder_uintptr_t data_ptr;
// 读取要释放的缓冲区地址
if (get_user(data_ptr, (binder_uintptr_t __user *)ptr))
return -EFAULT;
ptr += sizeof(binder_uintptr_t);
// 释放内核缓冲区
binder_free_buffer(proc, data_ptr);
break;
}
// ... 其他指令处理
}
// 更新已消耗的字节数
*consumed = ptr - buffer;
}
return 0;
}
这里有个容易踩坑的地方:用户空间传入的指令序列必须严格对齐。每个指令的参数长度是固定的,如果用户空间传入了非法指令或者参数长度不对,驱动会直接返回 -EINVAL。我曾经在调试一个三方应用时,发现它构造的 BC_TRANSACTION 指令后面跟的参数长度少了 4 个字节,结果 ioctl 一直返回错误,应用反复重试导致卡死。
27.4 事务创建:binder_transaction() 的完整链路
当指令是 BC_TRANSACTION 或 BC_REPLY 时,驱动会调用 binder_transaction()。这个函数是整个 Binder IPC 最复杂、最核心的部分。我画了一张流程图帮你理清思路:
流程看起来不复杂,但每个步骤背后都有大量细节。我挑几个关键点说说:
27.5 关键细节:缓冲区分配与数据拷贝
缓冲区分配是 binder_transaction() 里最容易出问题的地方。驱动会调用 binder_alloc_new_buf() 在目标进程的内核映射区分配一块内存。这块内存的大小等于用户空间传入的数据大小加上 Binder 事务头部的开销。
分配策略是这样的:
- 优先从目标进程的 空闲缓冲区列表 中查找大小合适的块
- 如果找不到,就从 预分配的内核映射区 中切一块出来
- 如果映射区也不够,就返回
-ENOSPC
我记得在 Android 8.0 之前,Binder 的缓冲区大小上限是 1MB。那时候有些应用会一次性传输大图片,经常触发 -ENOSPC 错误。后来 Google 把上限调整到了 4MB,但说实话,传输超过 1MB 的数据还是建议用共享内存或者文件描述符传递,别硬塞 Binder。
数据拷贝 用的是 copy_from_user()。这里有个性能优化的点:Binder 会把 binder_transaction_data 中的 data.ptr.buffer 和 data.ptr.offsets 指向的 扁平数据 和 偏移数组 分别拷贝到内核缓冲区。扁平数据里可能包含 Binder 对象(flat_binder_object),驱动在拷贝完成后会遍历偏移数组,逐个处理这些 Binder 对象——该增加引用的增加引用,该转换句柄的转换句柄。
个人经验:如果你在调试 Binder 传输大数据的性能问题,可以关注 copy_from_user() 的耗时。我曾经在某个项目里发现,传输 512KB 数据时,光拷贝就占了整个 ioctl 耗时的一半以上。后来我们改用 ashmem 共享内存来传递大块数据,性能提升了 3 倍。
27.6 事务挂载与唤醒
数据拷贝完成后,驱动会创建一个 binder_transaction 结构体,里面包含了事务的元信息(发起者、目标、cookie 等)。然后把这个事务挂到目标线程或目标进程的 todo 链表上。
挂载策略是这样的:
- 如果目标线程正在等待事务(处于
BINDER_LOOPER_STATE_WAITING状态),就直接挂到该线程的todo上 - 如果目标线程不在等待,就挂到目标进程的
todo上,由进程内的其他线程去取 - 如果是
BC_REPLY(回复),则挂到发起线程的todo上
挂载完成后,调用 wake_up_interruptible() 唤醒目标线程。嗯,这里要注意,唤醒操作是在 不持有目标线程锁 的情况下进行的,这是为了避免死锁。
避坑指南:我曾经遇到过一个诡异的问题——某个服务端线程被唤醒后,读取事务时发现 todo 链表是空的。排查了半天才发现,是因为唤醒操作和事务挂载之间存在竞态条件。虽然驱动层已经用 spinlock 保护了 todo 链表,但唤醒操作在释放锁之后才执行,导致线程被唤醒后可能还没看到新挂载的事务。解决方案是在挂载事务后、释放锁之前调用 wake_up,确保唤醒时事务已经可见。
27.7 写入完成后的读取
当 binder_thread_write() 处理完所有指令后,控制权回到 binder_ioctl_write_read()。如果 read_size > 0,驱动会接着调用 binder_thread_read() 来读取回复或通知。
这里有个设计上的巧妙之处:同步调用时,写入和读取是在同一个 ioctl 调用中完成的。发起方先写入 BC_TRANSACTION,然后立即进入读取循环,等待目标线程的回复。整个过程不会发生两次系统调用,效率很高。
读取部分的细节我们下一章再细聊。今天先总结一下写入流程的核心要点:
BINDER_WRITE_READ是 Binder IPC 的数据传输入口- 写入流程由
binder_thread_write()处理,逐个解析BC_XXX指令 BC_TRANSACTION/BC_REPLY会触发binder_transaction(),完成缓冲区分配、数据拷贝、事务挂载和唤醒- 同步调用时,写入和读取在同一个 ioctl 中完成
好了,数据发送的流程就讲到这里。下一章我们聊聊数据读取——也就是目标线程怎么把事务从 todo 链表里取出来,然后通过 BR_XXX 指令返回给用户空间。
公众号:蓝海资料掘金营,微信deep3321