Skip to content

Binder one-copy

追踪 Parcel 指针、目标 allocator、用户映射 buffer 与 copy_from_user,厘清 Binder one-copy 的真实边界。

基于android-17.0.0_r1
AndroidBinder内存

Binder one-copy ​

one-copy 的关键不是“没有拷贝”,而是发送方用户地址只被驱动读取一次,驱动把数据直接写入目标进程预分配的 Binder buffer;接收方随后直接消费该目标映射。下面的流程图对应 talkWithDriver、binder_transaction、binder_alloc_copy_user_to_buffer 和 BC_FREE_BUFFER。

本文面向已读过 Binder同步调用、Binder锁竞争 的读者。one-copy 不是“没有拷贝”,而是 Binder 驱动把数据直接复制到目标进程预先映射的 buffer,避免再建立一个独立的内核数据副本。本文沿 Parcel::ipcData、BC_TRANSACTION、目标 binder_alloc、copy_from_user 和接收端地址使用追踪这条链。

1. 发送指针 ​

源码文件:frameworks/native/libs/binder/IPCThreadState.cpp

cpp
tr.data_size = data.ipcDataSize();
tr.data.ptr.buffer = data.ipcData();
tr.offsets_size = data.ipcObjectsCount() * sizeof(binder_size_t);
tr.data.ptr.offsets = data.ipcObjects();

发送方只把 Parcel 的用户地址、数据长度和对象偏移表写入 binder_transaction_data,随后通过 BC_TRANSACTION 交给驱动;这一步没有把整段 Parcel 复制到内核临时缓冲区。

2. 目标分配 ​

源码文件:kernel/common/drivers/android/binder.c

c
t->buffer = binder_alloc_new_buf(&target_proc->alloc,
    tr->data_size, tr->offsets_size, extra_buffers_size,
    !reply && (t->flags & TF_ONE_WAY));

驱动根据目标进程的 binder_alloc 分配 buffer。buffer 同时具有内核描述结构和目标用户映射地址 user_data;数据页按需安装到该映射,不是发送方 Parcel 的地址直接暴露给接收方。

3. 用户拷贝 ​

源码文件:kernel/common/drivers/android/binder_alloc.c

c
ret = copy_from_user(kptr, from, size);

binder_alloc_copy_user_to_buffer 按页取得目标 buffer 的物理页并从发送方用户地址复制。所谓 one-copy 指这里一次跨地址空间复制直接落入目标 buffer;内核仍会执行页映射、对象 fixup 和安全上下文处理。

4. 接收地址 ​

源码文件:kernel/common/drivers/android/binder.c

c
trd->data.ptr.buffer = t->buffer->user_data;
trd->data.ptr.offsets = trd->data.ptr.buffer +
        ALIGN(t->buffer->data_size, sizeof(void *));

驱动向目标线程返回的是目标进程 mmap 区域中的用户地址。Native IPCThreadState 将该地址交给 Parcel::ipcSetDataReference,接收端直接在自己的映射中读取数据;驱动不再把 payload 复制回内核再复制给目标用户空间。

5. 对象修复 ​

one-copy 不适用于所有字段语义。Binder 对象偏移表中的 handle、local binder、fd、pointer 等需要驱动遍历和 fixup;binder_apply_fd_fixups、binder_translate_binder 等路径会修改目标 buffer 中的对象表示。数据 payload 少一次中间复制,不代表 Binder 对象可以原样跨进程使用。

6. 安全上下文 ​

启用 transaction security context 时,驱动为 SELinux context 增加 extra_buffers_size,把上下文追加到同一目标 buffer。它仍属于目标 allocator 的空间,接收端通过 BR_TRANSACTION_SEC_CTX 获取元数据;one-copy 不绕过安全检查和 context 复制。

7. 释放生命周期 ​

源码文件:frameworks/native/libs/binder/IPCThreadState.cpp

接收端 Parcel::ipcSetDataReference 注册 freeBuffer 回调。Parcel 被释放后发送 BC_FREE_BUFFER,驱动执行 binder_free_buf,清理 transaction、续接 async 队列并回收 allocator buffer。接收端必须在 buffer 生命周期内完成读取,不能把 data 指针保存到回调之后。

8. 失败边界 ​

  • 目标进程没有有效 mmap:binder_alloc_new_buf 返回 -ESRCH,事务转为 BR_DEAD_REPLY。
  • 空间不足:返回 -ENOSPC,同步调用得到失败 reply;oneway 发送方没有业务 reply。
  • 用户地址不可读:copy_from_user 返回错误,驱动清理事务并发送 BR_FAILED_REPLY。
  • 对象偏移或 fd fixup 非法:目标 buffer 可能已分配,但事务在交付前被清理,不能把分配成功当作调用成功。

9. 与零拷贝区别 ​

发送端 Parcel 到目标 buffer 仍发生一次 copy_from_user;目标端从自身映射读取,不再发生第二次 payload copy。真正的大块共享数据应使用 ashmem、共享内存或 file descriptor,把 Binder 只作为句柄传输;否则 one-copy 仍受目标 buffer 上限和页分配成本限制。

10. 观测路径 ​

源码文件:kernel/common/drivers/android/binder_trace.h

用 binder_transaction_alloc_buf、binder_transaction_received、binder_transaction_buffer_release tracepoint 对齐分配、交付和释放时间;debugfs 的 buffer/async space 观察目标进程占用。若分配耗时高,结合 binder_alloc 的 alloc mutex 和页安装路径,而不是只看 Parcel 序列化时间。

11. 阅读检查 ​

从 Parcel::ipcData() 画到接收端 ipcSetDataReference,标出唯一 payload copy、对象 fixup、目标 mmap 和 BC_FREE_BUFFER。再分别代入空间不足、用户地址不可读和传 fd 三种情况,指出哪一步改变了 one-copy 的成本或失败状态。