28. 数据接收:Binder 驱动如何将数据从内核拷贝到用户空间?

好,我们接着聊。前面几章我们把数据从用户空间送到内核,又在内核里完成了目标查找和权限校验。那现在数据已经躺在目标进程的内核缓冲区里了,接下来最关键的一步——怎么把它安全、高效地拷贝到用户空间?

这个问题,说白了就是Binder IPC的最后一百米。我早年调试一个多媒体服务时,就遇到过数据明明到了内核,用户态却死活收不到的情况。嗯,当时排查了很久,最后发现是接收线程的等待队列出了问题。所以这块儿,值得你花点心思。

数据接收的整体流程

先给你画个全景图,看看数据从内核到用户空间到底走了哪几步。

Binder驱动数据接收流程 用户空间(接收进程) ioctl() 等待 → 被唤醒 → 读取数据 内核空间(Binder驱动) binder_thread_read() 执行拷贝 内核缓冲区(binder_buffer) 数据已由发送方拷贝至此,等待接收方读取 copy_to_user() 将数据从内核拷贝到用户空间

你看,整个流程其实就三步:用户线程在ioctl里等着,驱动把数据从内核缓冲区取出来,最后通过copy_to_user送到用户空间。但这里面有几个关键细节,我一个个给你拆开讲。

核心函数:binder_thread_read

数据接收的核心函数是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)

参数看着多,其实就几类:

  • proc/thread:标识哪个进程、哪个线程在收数据
  • binder_buffer:用户空间传下来的缓冲区地址(就是ioctl里的那个参数)
  • size:缓冲区大小
  • consumed:实际消耗了多少字节,返回给用户
  • non_block:是否非阻塞模式

我个人习惯把这个函数理解成一个「数据搬运工」——它从内核的待处理队列里取出一个事务,然后小心翼翼地搬到用户空间。

等待机制:没有数据怎么办?

如果当前没有数据可读,线程会进入等待状态。这部分逻辑在binder_thread_read的开头:

// 简化后的等待逻辑
while (1) {
    // 检查是否有待处理的事务
    if (!binder_has_work(thread, proc)) {
        if (non_block) {
            return -EAGAIN;  // 非阻塞模式直接返回
        }
        // 阻塞等待
        binder_wait_for_work(thread, proc);
    }
    // 有数据了,开始处理
    break;
}

这里有个坑,我曾经踩过。在Android 8.0之前,等待机制用的是wait_event_interruptible,但后来改成了wait_event_freezable。为什么?因为系统休眠时,如果线程还在等Binder数据,会导致系统无法正常挂起。这个改动就是为了解决休眠唤醒时的死锁问题。

注意:Binder的等待队列是每个线程独立的。如果你在用户态开了多个线程同时做Binder调用,每个线程都有自己的等待队列,互不干扰。但这也意味着——如果线程A在等数据,线程B却拿到了本该给A的数据,那就出问题了。好在Binder驱动通过事务的target_thread字段保证了精确投递。

数据拷贝:copy_to_user的玄机

当驱动确认有数据可读后,就开始执行真正的拷贝操作。核心代码大致如下:

// 从内核缓冲区取出事务
struct binder_transaction *t = list_first_entry(&thread->todo, 
                                                struct binder_transaction, 
                                                todo);

// 计算需要拷贝的数据大小
size_t data_size = t->buffer->data_size;
size_t offsets_size = t->buffer->offsets_size;

// 执行拷贝到用户空间
if (copy_to_user(ptr, t->buffer->data, data_size)) {
    // 拷贝失败处理
    return -EFAULT;
}
ptr += data_size;

if (copy_to_user(ptr, t->buffer->offsets, offsets_size)) {
    return -EFAULT;
}
ptr += offsets_size;

这里有个关键点:Binder把数据分成了两部分拷贝——先是真正的数据内容,然后是对象偏移表。为什么这么设计?你想想看,用户空间拿到数据后,需要知道哪些位置是Binder对象(比如文件描述符、Binder引用),偏移表就是干这个用的。

性能要点:copy_to_user本身是有开销的。对于大数据块,一次拷贝几KB甚至几十KB,性能影响很明显。所以Binder驱动在拷贝前会做一次检查——如果数据量超过一定阈值,会触发内存映射优化,直接通过共享内存的方式传递,避免显式拷贝。这个阈值在不同内核版本里不一样,我记得在Android 12上大约是64KB。

对象处理的特殊逻辑

数据里如果包含Binder对象(比如你传了一个IBinder接口),驱动在拷贝时还要做额外处理:

// 遍历偏移表,处理每个Binder对象
for (size_t i = 0; i < num_offsets; i++) {
    struct flat_binder_object *obj;
    obj = (struct flat_binder_object *)(data + offsets[i]);
    
    switch (obj->hdr.type) {
        case BINDER_TYPE_BINDER:
            // 本地Binder对象,增加引用计数
            binder_inc_ref_for_node(proc, obj, ...);
            break;
        case BINDER_TYPE_HANDLE:
            // 远程Binder句柄,增加句柄引用
            binder_inc_ref_for_handle(proc, obj, ...);
            break;
        case BINDER_TYPE_FD:
            // 文件描述符,需要复制文件描述符
            binder_translate_fd(obj, t, thread, ...);
            break;
    }
}

这个处理过程是在拷贝到用户空间之前完成的。驱动先在内核态把对象的引用关系处理好,然后再把整个数据块拷贝出去。这样用户空间拿到数据时,所有的Binder对象已经是有效状态了。

避坑指南:我曾经遇到过一个bug——用户空间收到数据后,访问里面的Binder对象时崩溃了。排查发现,是因为驱动在拷贝过程中,某个Binder对象的引用计数更新失败了,导致对象被提前释放。解决方案是在驱动层加了事务锁,确保引用计数的更新和拷贝操作是原子的。所以如果你在写内核模块,记得处理好并发问题。

数据接收完成后的清理

数据拷贝完成后,驱动还要做一些收尾工作:

  • 释放内核缓冲区:把binder_buffer标记为空闲,放回空闲链表
  • 更新统计信息:记录本次传输的数据量、耗时等
  • 唤醒发送方:如果发送方在等待回复,现在可以唤醒它了

这部分逻辑在binder_free_transaction函数里。我建议你去看一下内核源码,这个函数的注释写得特别清楚,每一步都有说明。

总结一下

数据从内核拷贝到用户空间,核心就是binder_thread_read + copy_to_user的组合。驱动先通过等待队列让线程休眠,有数据时唤醒,然后小心翼翼地处理Binder对象引用,最后把数据完整地搬到用户空间。

嗯,这里要注意的是——整个拷贝过程是在进程上下文中执行的,也就是说它占用的是调用线程的时间片。如果数据量特别大,可能会导致线程被卡住一会儿。所以实际开发中,我建议大块数据走共享内存或者文件描述符传递,别硬扛Binder的拷贝开销。

好了,数据接收这块儿就聊到这儿。下一章我们聊聊Binder的死亡通知机制——当服务端挂了,客户端怎么优雅地知道?


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