Binder分散聚集传输
Binder 的 scatter-gather(SG)不是“把一次 ioctl 拆成多次拷贝”,也不是所有 Parcel 默认都会经过的性能模式。Android 17 的驱动提供 BC_TRANSACTION_SG/BC_REPLY_SG 结构入口;在统一 binder_transaction() 内,真正触发分散数据处理的是 Parcel offsets 中的 BINDER_TYPE_PTR 和 BINDER_TYPE_FDA 对象。驱动先把源用户地址、目标 buffer 偏移和后续指针/FD 修正记录下来,再在对象扫描完成后统一执行 deferred copies。
本文面向已经读过 BC事务写入、Binder事务发送、binder_alloc内存分配器 和 binder_buffer管理 的读者。本文不重复普通 flat Binder 对象的引用翻译,也不把 UAPI 中存在 SG 命令写成当前 libbinder 普通生产路径一定使用 SG;重点是解释 buffers_size 如何进入统一事务入口,sgc_head/pf_head 如何形成,为什么 fixup 必须延迟,以及数据、指针和 FDA 失败时怎样回滚。
读完后,应该能定位 BINDER_TYPE_PTR 的父对象校验、说明 binder_defer_copy() 为什么要求按目标偏移有序、解释 skip_size 与 fixup_data 的差异,并判断一次 SG 失败需要清理哪些链表、对象引用和目标 buffer。
下面这张图先回答一个核心问题:SG 事务为什么不能在扫描到 BINDER_TYPE_PTR 或 BINDER_TYPE_FDA 时立即把所有数据写完。图中的扫描、记录、统一执行和回滚分别对应正文的 binder_transaction()、binder_defer_copy()、binder_do_deferred_txn_copies() 与错误标签。
这里的关键不变量是:对象扫描阶段只建立“稍后复制什么、稍后改哪里”的有序描述;只有全部对象验证成功后,目标进程才会看到完成 fixup 的结构。任何中途错误都不能越过 BR_TRANSACTION/BR_REPLY 的交付边界。
1. 入口边界
1.1 UAPI结构
源码文件:kernel/common/include/uapi/linux/android/binder.h
相关类型:binder_transaction_data_sg、binder_buffer_object、binder_fd_array_object
struct binder_transaction_data_sg {
struct binder_transaction_data transaction_data;
binder_size_t buffers_size;
};
struct binder_buffer_object {
struct binder_object_header hdr;
__u32 flags;
binder_uintptr_t buffer;
binder_size_t length;
binder_size_t parent;
binder_size_t parent_offset;
};
struct binder_fd_array_object {
struct binder_object_header hdr;
__u32 pad;
binder_size_t num_fds;
binder_size_t parent;
binder_size_t parent_offset;
};buffers_size 是 SG 命令相对普通 transaction descriptor 增加的总额外空间;binder_buffer_object 描述一段需要复制到目标 buffer 的用户内存,binder_fd_array_object 描述嵌在 parent buffer 中、需要逐个翻译的 FD 数组。三者不是同一层的对象:前者是命令 envelope 字段,后两者是 Parcel offsets 中的对象类型。
1.2 命令分派
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_thread_write()
case BC_TRANSACTION_SG:
case BC_REPLY_SG: {
struct binder_transaction_data_sg tr;
if (copy_from_user(&tr, ptr,
sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
binder_transaction(proc, thread,
&tr.transaction_data,
cmd == BC_REPLY_SG,
tr.buffers_size);
break;
}
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, 0);
break;
}SG 与普通命令最终进入同一个 binder_transaction();差异只是在 SG 命令中把 buffers_size 传入 extra_buffers_size 参数。reply 仍由 transaction stack 决定,目标 handle、权限、冻结和 buffer allocator 也仍走同一主线。
1.3 用户态边界
源码文件:frameworks/native/libs/binder/IPCThreadState.cpp
相关函数:IPCThreadState::transact()
err = writeTransactionData(
BC_TRANSACTION, flags, handle,
code, data, nullptr);
if ((flags & TF_ONE_WAY) == 0) {
err = waitForResponse(reply);
} else {
err = waitForResponse(nullptr, nullptr);
}当前 frameworks/native 的普通 transact() 写的是 BC_TRANSACTION,而不是 BC_TRANSACTION_SG。因此“驱动支持 SG”与“普通 libbinder 调用会自动选择 SG”必须分开描述;本篇的 SG 入口主要由 UAPI/驱动结构和包含 PTR/FDA 对象的 transaction 逻辑定义。
2. 两条链表
2.1 拷贝描述
源码文件:kernel/common/drivers/android/binder.c
相关类型:binder_sg_copy
struct binder_sg_copy {
binder_size_t offset;
const void __user *sender_uaddr;
size_t length;
struct list_head node;
};binder_sg_copy 记录“从发送方哪个用户地址复制多少字节到目标 buffer 哪个 offset”。它不保存目标 page 指针,也不立即执行 copy;目标页仍由 binder_alloc owner 管理,真正写入延迟到所有对象关系被检查之后。
2.2 Fixup描述
源码文件:kernel/common/drivers/android/binder.c
相关类型:binder_ptr_fixup
struct binder_ptr_fixup {
binder_size_t offset;
size_t skip_size;
binder_uintptr_t fixup_data;
struct list_head node;
};skip_size != 0 表示 copy 到这个目标 offset 时跳过一段源数据,典型用途是 FDA 的 FD 数组:这段位置稍后由目标进程上下文中的 FD 翻译写入。skip_size == 0 时,fixup_data 是要直接写入目标 buffer 的新指针,例如 parent buffer 指向子 buffer 的目标地址。
2.3 有序不变量
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_defer_copy()、binder_add_fixup()
list_add_tail(&bc->node, sgc_head);
list_for_each_entry_reverse(tmppf,
pf_head, node) {
if (tmppf->offset < pf->offset) {
list_add(&pf->node,
&tmppf->node);
return 0;
}
}
list_add(&pf->node, pf_head);SG copy 按目标 offset 单调追加,因此可以直接 tail insert;fixup 大部分按序但存在例外,所以从链尾反向寻找插入点,最终保证 pf_head 按 offset 排序。deferred copy 依赖这个顺序,一旦 fixup 无序,copy loop 就无法判断下一段应该复制、跳过还是写新指针。
3. PTR对象
3.1 子Buffer空间
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_transaction() 中 BINDER_TYPE_PTR 分支
case BINDER_TYPE_PTR: {
struct binder_buffer_object *bp =
to_binder_buffer_object(hdr);
binder_size_t sg_buf_offset =
ALIGN(sg_buf_offset,
sizeof(u64));
if (bp->length >
sg_buf_end_offset - sg_buf_offset)
goto err_bad_offset;
ret = binder_defer_copy(
&sgc_head, sg_buf_offset,
(const void __user *)
(uintptr_t)bp->buffer,
bp->length);
if (ret)
goto err_translate_failed;
sg_buf_offset +=
ALIGN(bp->length,
sizeof(void *));
}PTR 子 buffer 被放在目标 transaction buffer 的 extra 区域,sg_buf_offset 按 64 位边界对齐并递增。驱动只把源地址和长度记入 sgc_head,不在遇到对象的瞬间把整段源内存复制过去;这样 parent fixup 和后续 FDA 区域可以一起参与最终写入。
3.2 父对象
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_fixup_parent()
if (!(bp->flags &
BINDER_BUFFER_FLAG_HAS_PARENT))
return 0;
parent = binder_validate_ptr(
target_proc, b, &object,
bp->parent, off_start_offset,
&parent_offset, num_valid);
if (!parent)
return -EINVAL;
if (!binder_validate_fixup(
target_proc, b, off_start_offset,
parent_offset, bp->parent_offset,
last_fixup_obj_off,
last_fixup_min_off))
return -EINVAL;
buffer_offset = bp->parent_offset +
parent->buffer - b->user_data;
return binder_add_fixup(
pf_head, buffer_offset,
bp->buffer, 0);父对象关系把“源 Parcel 中的指针值”转换成“目标 Binder buffer 中子对象的地址”。驱动先验证 parent index、parent offset 和 fixup 顺序,再把目标写入位置及新指针值加入 pf_head;未通过校验时不会让未经修正的源地址暴露给接收方。
3.3 Fixup生效
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_do_deferred_txn_copies()
copy_size = pf
? min(bytes_left,
(size_t)pf->offset - offset)
: bytes_left;
if (!ret && copy_size)
ret = binder_alloc_copy_user_to_buffer(
alloc, buffer, offset,
sgc->sender_uaddr + bytes_copied,
copy_size);
if (copy_size != bytes_left) {
if (pf->skip_size) {
bytes_copied += pf->skip_size;
} else if (!ret) {
ret = binder_alloc_copy_to_buffer(
alloc, buffer, pf->offset,
&pf->fixup_data,
sizeof(pf->fixup_data));
bytes_copied +=
sizeof(pf->fixup_data);
}
list_del(&pf->node);
kfree(pf);
}copy loop 每次只复制到下一个 fixup 边界。遇到 skip_size 时跳过源中的 FDA 槽位;遇到 fixup_data 时写入目标地址。这样目标 buffer 在完成一次 deferred copy 后,不会留下源进程的子 buffer 指针。
4. FDA对象
4.1 数组边界
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_translate_fd_array()
fd_buf_size = sizeof(u32) * fda->num_fds;
if (fda->num_fds >=
SIZE_MAX / sizeof(u32))
return -EINVAL;
if (fd_buf_size > parent->length ||
fda->parent_offset >
parent->length - fd_buf_size)
return -EINVAL;FDA 不能简单当作 PTR 子 buffer。驱动要先确认 fd 数量乘法不会溢出,再确认 parent buffer 中从 parent_offset 开始有足够空间容纳所有 fd。边界失败发生在目标 fd 安装之前,不能把源整数直接当作目标进程 fd。
4.2 跳过与翻译
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_translate_fd_array()
fda_offset = parent->buffer -
t->buffer->user_data +
fda->parent_offset;
sender_ufda_base =
(void __user *)(uintptr_t)
sender_uparent->buffer +
fda->parent_offset;
ret = binder_add_fixup(
pf_head, fda_offset, 0,
fda->num_fds * sizeof(u32));
if (ret)
return ret;FDA 先在 pf_head 中标记一段 skip 区域,再逐个从发送方 parent 子 buffer 读源 fd,调用 binder_translate_fd() 为目标 proc 申请并记录目标 fd。deferred copy 只负责避开这段区域,目标 fd 写入由 FD fixup 的目标上下文完成。
4.3 成功交付
FDA 翻译成功后,目标 buffer 中的数组槽位保存目标 proc 的 fd;源进程 fd 表和目标进程 fd 表不是同一组整数。用户态收到 Parcel 后负责使用和关闭目标 fd,失败路径则由 transaction/buffer release 根据 is_failure 判断哪些 fixup 已真正安装。
5. 统一执行
5.1 普通数据尾部
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_transaction()
if (binder_alloc_copy_user_to_buffer(
&target_proc->alloc, t->buffer,
user_offset,
user_buffer + user_offset,
tr->data_size - user_offset)) {
return_error = BR_FAILED_REPLY;
goto err_copy_data_failed;
}对象扫描过程中,驱动已经把每个普通数据段复制到对象前;对象处理完后再复制剩余 data 尾部。SG 链表只覆盖 PTR/FDA 触发的分散区段,不意味着整个 transaction 的 data 都会进入 sgc_head。
5.2 Deferred copy
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_do_deferred_txn_copies()
ret = binder_do_deferred_txn_copies(
&target_proc->alloc, t->buffer,
&sgc_head, &pf_head);
if (ret) {
return_error = BR_FAILED_REPLY;
return_error_param = ret;
goto err_copy_data_failed;
}只有普通数据、对象翻译和 fixup 记录都完成后,驱动才执行 deferred copy。copy 返回正值会被归一为 -EINVAL;负值通常来自用户地址、目标 buffer 或 page copy 失败。
5.3 可见性保证
SG 的核心不是减少某个固定次数的 copy,而是把“复制源数据”和“写入目标指针/FD”安排在同一个受控阶段。直到 binder_do_deferred_txn_copies() 完成,目标 buffer 中的 PTR/FDA 相关区域不会以未经翻译的源值作为最终可消费状态;失败时则进入统一 cleanup,不向目标线程投递 BR_TRANSACTION。
6. 失败回滚
6.1 链表释放
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_cleanup_deferred_txn_lists()
list_for_each_entry_safe(sgc,
tmpsgc, sgc_head, node) {
list_del(&sgc->node);
kfree(sgc);
}
list_for_each_entry_safe(pf,
tmppf, pf_head, node) {
list_del(&pf->node);
kfree(pf);
}对象校验、父对象校验或内存分配失败时,尚未消费的 SG copy/fixup 描述符必须全部释放。这个函数只释放链表元数据,不反向减少已经完成的 node/ref/FD 翻译;那些动作由 transaction buffer release 和 FD fixup cleanup 负责。
6.2 已处理对象
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_transaction() 错误标签
binder_cleanup_deferred_txn_lists(
&sgc_head, &pf_head);
binder_free_txn_fixups(t);
binder_transaction_buffer_release(
target_proc, NULL, t->buffer,
buffer_offset, true);
t->buffer->transaction = NULL;
binder_alloc_free_buf(
&target_proc->alloc, t->buffer);失败回滚只把 buffer_offset 之前已经处理的 offsets 对象交给 release;后面的对象没有被验证,也不能假设已经增加了引用。目标 buffer 仍需交回 allocator,防止 SG extra 空间和 pages 泄漏。
6.3 FD差异
binder_translate_fd_array() 中已经成功申请的目标 fd 会挂在 transaction fixup 列表;翻译失败时 binder_free_txn_fixups() 释放未安装 fixup,已安装或已经写入目标 buffer 的部分则由相应 release 路径处理。不能简单按 num_fds 全量关闭源 fd,也不能把目标 fd 当作源 fd 重复关闭。
7. 释放边界
7.1 正常交付
源码文件:kernel/common/drivers/android/binder.c
相关函数:binder_thread_read()、binder_free_buf()
SG transaction 成功完成 deferred copy 后,和普通 transaction 一样生成 BR_TRANSACTION 或 BR_REPLY。用户态处理 Parcel 并写 BC_FREE_BUFFER,驱动再扫描 offsets 释放 node/ref 边沿,最后 allocator 回收整个 buffer。SG 只改变构造阶段的数据来源和 fixup,不改变 buffer 的最终 owner。
7.2 descriptor失败
descriptor 写入失败时,目标线程没有收到 buffer 指针,驱动不会等待用户 BC_FREE_BUFFER。此时 SG 链表早已在 transaction 构造阶段被消费;当前分支只执行普通的 transaction cleanup,不能把 descriptor -EFAULT 写成再次清理 SG 链表,也不能重试同一 descriptor 来“补发” SG 数据。
7.3 用户可见错误
| 阶段 | 失败 | 结果 |
|---|---|---|
| SG envelope | copy_from_user 失败 | BC 写入返回 -EFAULT |
| PTR/FDA 校验 | parent、长度、对齐或顺序错误 | BR_FAILED_REPLY/失败 transaction |
| deferred copy | 源地址或目标页复制失败 | 清理链表和已处理对象 |
| FD translation | 源 fd 无效或目标不接受 | 释放 fixup,事务失败 |
| 交付后释放 | BC_FREE_BUFFER 地址错误 | allocator 不释放该 buffer |
8. 测试边界
8.1 PTR对象拒绝
源码文件:frameworks/native/libs/binder/tests/binderLibTest.cpp
相关测试:BinderLibTest.BufRejected
binder_buffer_object obj {
.hdr = { .type = BINDER_TYPE_PTR },
.flags = 0,
.buffer = reinterpret_cast<
binder_uintptr_t>(&buf),
.length = 4,
};
data.writeFileDescriptor(0);
memcpy(const_cast<uint8_t*>(
data.data()), &obj, sizeof(obj));
data.setDataSize(sizeof(obj));
EXPECT_THAT(server->transact(
BINDER_LIB_TEST_REJECT_OBJECTS,
data, &reply),
Not(StatusEq(NO_ERROR)));测试手工覆盖 Parcel 对象表中的对象头,构造一个指向本地栈变量的 PTR 对象,并断言 transaction 不能成功。它覆盖用户构造的非法 PTR/对象拒绝,不证明合法 parent/fixup 或真实 SG envelope 的性能。
8.2 大数据邻界
源码文件:frameworks/native/libs/binder/tests/binderLibTest.cpp
相关测试:GargantuanVectorSent、LimitExceededVectorSent
测试发送接近 Binder buffer 上限的 vector 并检查 echo;再发送超过上限的 vector,断言返回 FAILED_TRANSACTION。这能帮助定位 SG/extra buffer 与 allocator 连续空间的边界,但测试本身没有断言每一次都选择 BC_TRANSACTION_SG,也不能把接近上限的成功写成 SG 专属行为。
8.3 驱动入口
源码文件:kernel/common/drivers/android/binder.c
相关命令:BC_TRANSACTION_SG、BC_REPLY_SG
当前 driver interface 测试覆盖 UAPI 命令解析和标准 transaction 序列,但没有一个公开测试把所有 PTR/FDA、parent fixup、deferred copy 和 FD rollback 组合成完整成功样例。因此这篇文章把源码分支和失败测试分开陈述,不把“存在 SG 命令”写成“普通 libbinder 已在生产路径启用 SG”。
9. 源码导航
遇到包含嵌套 buffer 或 native handle 的事务失败时,可以按这个顺序阅读:
- 先看 envelope 是
BC_TRANSACTION还是BC_TRANSACTION_SG,确认buffers_size是否进入extra_buffers_size; - 在
binder_transaction()找off_start_offset、sg_buf_offset和sg_buf_end_offset,判断 extra 区域是否越界; - 对每个
BINDER_TYPE_PTR检查 parent、length、fixup 顺序和binder_defer_copy(); - 对
BINDER_TYPE_FDA检查 parent array 边界、fd 数量、源 fd 和binder_translate_fd(); - 进入
binder_do_deferred_txn_copies(),观察 copy 是否在 fixup offset 处正确分段; - 失败时检查
binder_cleanup_deferred_txn_lists()、binder_free_txn_fixups()、binder_transaction_buffer_release()的覆盖范围; - 最后回到
binder_alloc_free_buf(),确认目标 buffer 和 extra pages 没有泄漏。
源码搜索:
rg -n "BC_TRANSACTION_SG|BC_REPLY_SG|buffers_size|binder_transaction\\(" \
kernel/common/drivers/android/binder.c \
kernel/common/include/uapi/linux/android/binder.h
rg -n "binder_defer_copy|binder_add_fixup|binder_do_deferred_txn_copies" \
kernel/common/drivers/android/binder.c
rg -n "BINDER_TYPE_PTR|BINDER_TYPE_FDA|binder_translate_fd_array|binder_fixup_parent" \
kernel/common/drivers/android/binder.c \
kernel/common/include/uapi/linux/android/binder.h
rg -n "BufRejected|GargantuanVectorSent|LimitExceededVectorSent" \
frameworks/native/libs/binder/tests/binderLibTest.cpp如果能解释“为什么 SG 不是普通 ioctl 的多次拷贝”“为什么 skip_size 不能直接写成 fixup pointer”“为什么 deferred copy 必须在对象扫描之后执行”“为什么失败回滚要同时处理链表、FD fixup、对象引用和 allocator buffer”,就已经掌握了 Android 17 Binder SG 路径的源码边界。
