RFC-0010:对 zx_channel_write 和 zx_channel_call 的 zx_channel_iovec_t 支持 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 此 RFC 为 zx_channel_write 和 zx_channel_call 引入了一种新模式,该模式会从多个内存区域(而非单个连续缓冲区)复制输入数据。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2020-09-25 |
审核日期(年-月-日) | 2020-10-06 |
摘要
此 RFC 为 zx_channel_write
、zx_channel_write_etc
、zx_channel_call
和 zx_channel_call_etc
引入了一种新模式,该模式会从多个内存区域(而非单个连续缓冲区)复制输入数据。这允许直接从多个用户空间对象复制消息数据,而无需中间分配、复制和布局步骤,从而提高了某些用户 / 客户端的性能。为此,需要更新现有的系统调用,以便在指定选项时接受 zx_channel_iovec_t
内存区域描述符数组。
设计初衷
提出此提案的主要动机是性能。
对于非线性化域对象,FIDL 绑定目前需要执行以下操作:(1) 分配缓冲区,(2) 将对象复制到标准布局的缓冲区中。完成这些步骤后,系统会再次将缓冲区复制到内核。zx_channel_iovec_t
允许将对象直接复制到内核。此外,FIDL 消息数据不再需要按标准顺序排列,只有 zx_channel_iovec_t
数组必须反映所需的顺序。
设计
zx_channel_write 目前具有以下签名:
zx_status_t zx_channel_write(zx_handle_t handle,
uint32_t options,
const void* bytes,
uint32_t num_bytes,
const zx_handle_t* handles,
uint32_t num_handles);
输入数据是 bytes
指向的连续字节数组。在 zx_channel_write_etc
、zx_channel_call
和 zx_channel_call_etc
中,存在类似的数组。由于这些数组必须是连续的,因此会产生开销。特别是,对于包含离线组件的 FIDL 消息,FIDL 编码器必须分配一个缓冲区并将数据重新定位到其中,这可能需要付出高昂的代价。
zx_channel_iovec_t
提供了另一种路径。zx_channel_write
、zx_channel_write_etc
、zx_channel_call
和 zx_channel_call_etc
会改为接收对象位置和大小的列表,并且复制会在内核中进行,从而避免额外的复制和分配。
zx_channel_iovec_t
在 C++ 中的定义如下:
typedef struct zx_channel_iovec {
void* buffer; // User-space bytes.
uint32_t capacity; // Number of bytes.
uint32_t reserved; // Reserved.
} zx_channel_iovec_t;
每个 zx_channel_iovec_t
都指向要从 buffer
复制到内核消息缓冲区的下一个 capacity
字节。reserved
必须设为零。只有当 capacity
为 0 时,buffer
字段才可以为 NULL。buffer
指针可以在多个 zx_channel_iovec_t
中重复。
zx_channel_write
、zx_channel_write_etc
、zx_channel_call
或 zx_channel_call_etc
的签名保持不变。不过,当用户为这些系统调用指定 ZX_CHANNEL_WRITE_USE_IOVEC
选项时,void* bytes
实参将被解释为 zx_channel_iovec_t*
。同样,num_bytes
实参将被解读为数组中的 zx_channel_iovec_t
数量。
请注意,句柄数组的类型(zx_handle_t
或 zx_handle_disposition_t
)无关紧要,因为只会更改 bytes
数组。
zx_channel_iovec_t
数组描述的消息要么包含消息的所有部分一并发送,要么根本不发送。无论调用成功与否,调用方都无法再使用向系统调用提供的句柄。
错误情况
以下是 zx_channel_write
、zx_channel_write_etc
、zx_channel_call
和 zx_channel_call_etc
的错误条件,由于引入了 iovec,这些条件已更新。
ZX_ERR_OUT_OF_RANGE num_bytes
或 num_handles
分别大于 ZX_CHANNEL_MAX_MSG_BYTES
或 ZX_CHANNEL_MAX_MSG_HANDLES
。如果指定了 ZX_CHANNEL_WRITE_USE_IOVEC
选项,如果 num_bytes
大于 ZX_CHANNEL_MAX_MSG_IOVEC
或 iovec 容量的总和超过 ZX_CHANNEL_MAX_MSG_BYTES
,则会生成 ZX_ERR_OUT_OF_RANGE。
ZX_ERR_INVALID_ARGS bytes
是一个无效的指针,handles
是一个无效的指针,或者 options
包含无效的选项位。如果指定了 ZX_CHANNEL_WRITE_USE_IOVEC
选项,则如果 buffer
字段包含无效的指针,则会返回 ZX_ERR_INVALID_ARGS。
对齐方式
对 zx_channel_iovec_t
中指定的字节没有对齐限制。每个 zx_channel_iovec_t
都将被复制,但不会填充。
限制
每条消息的字节数 (65536
) 和句柄数 (64
) 的现有限制保持不变。请注意,这些限制适用于消息,而非 zx_channel_iovec_t
条目。
每个系统调用中的 zx_channel_iovec_t
数量将限制为 8192
。此数值来自可放入 65536
字节消息中的 8 字节对齐的代码内 + 代码外对象的数量,每个代码内 + 代码外对象都可能使用 zx_channel_iovec_t
条目。
实现
Syscall
- 引入
zx_channel_iovec_t
类型(如设计部分中所定义)。 - 添加
ZX_CHANNEL_WRITE_USE_IOVEC
- 可见的系统调用接口没有任何更改,
zx_channel_iovec_t
数组会传入到现有的bytes
参数。
内核
收到 ZX_CHANNEL_WRITE_USE_IOVEC
选项后,内核将执行以下操作:
- 将
zx_channel_iovec_t
对象指向的数据复制到消息缓冲区。虽然复制操作通常也会按zx_channel_iovec_t
输入的顺序执行,但这并非强制性要求。不过,最终消息必须按zx_channel_iovec_t
条目的顺序排列。 - 将消息写入该通道。
FIDL
这是一份系统调用更改提案,其实现在内核中进行,并且 FIDL 绑定更改的具体细节不在提案范围内。尽管如此,为了评估此提案,了解对 FIDL 编码的影响非常重要。
FIDL 绑定可以选择利用 zx_channel_iovec_t
支持,方法是添加对将 FIDL 对象编码为 zx_channel_iovec_t
数组的支持。
此编码路径与现有编码路径之间的一个关键区别在于,zx_channel_iovec_t
允许内核就地复制对象。这方面的主要复杂性在于指针。需要将 FIDL 编码的消息发送到内核,并将指针替换为 PRESENT
或 ABSENT
标记值。不过,在许多情况下,对象在系统调用后仍需要保留原始指针值,以便调用析构函数。
这意味着,利用 zx_channel_iovec_t
的绑定有时需要执行额外的记账工作,以确保正确清理对象。
Migration
由于此功能是作为默认停用的选项实现的,因此对现有用户的影响应该不会立即显现。调用站点可以根据需要迁移以使用该选项。
实际上,目的是迁移可以从 zx_channel_iovec_t
中受益的 FIDL 绑定,以便使用它。这对 FIDL 用户的影响预计很小。
性能
实现并对原型进行了基准测试。
此原型在内核端实现了 zx_channel_write 选项,并提供了有限的 FIDL 支持(仅限内嵌对象和矢量)。消息标头以及每个内嵌对象和外部对象都有一个 zx_channel_iovec_t
条目。在内核和 FIDL 编码中,使用一个包含 64 个条目的数组来存储 zx_channel_iovec_t
。
这些测量结果来自搭载 2.60GHz Intel Core i5-7300U CPU 的机器。
字节矢量事件基准测试 (zx_channel_write、zx_channel_wait_async 和 zx_channel_read) 取得了显著改进:
- 4096 字节矢量:9398 纳秒 -> 4495 纳秒
- 256 字节矢量:8788 纳秒 -> 3794 纳秒
FIDL 编码也表现出性能提升。
字节矢量示例的编码时间:
- 4096 字节矢量:345 纳秒 -> 88 纳秒
- 256 字节矢量:251 纳秒 -> 86 纳秒
内嵌对象的编码性能也有所提升:
- 包含 256 个 uint8 字段的结构体:67 纳秒 -> 49 纳秒
安全注意事项
由于这对现有系统调用进行了重大更改,因此在实现之前需要进行安全审核。
隐私注意事项
对隐私应该没有影响。
测试
系统会为每个发生更改的图层添加单元测试和集成测试。
我们不打算添加任何设备级或系统级端到端测试,但现有的测试覆盖率有助于确保迁移后不会引入任何意外 bug。
文档
需要更新系统调用文档,以指明支持此功能。
无需对整个架构的文档进行任何更改。
缺点、替代方案和未知情况
此方案的主要缺点是需要在内核中支持该选项,这会增加复杂性;对于使用 ZX_CHANNEL_WRITE_USE_IOVEC
选项的 FIDL 绑定,还会增加实际复杂性,因为需要确保对象在发生变异以进行原地复制后得到妥善清理。
限制
有一种观点认为 zx_channel_iovec_t
的数量应设有下限,该下限可能比 8192
更接近 16
。这样一来,zx_channel_iovec_t
数组便可复制到内核的堆栈上。不过,这会阻止按线下 FIDL 对象分配一个 zx_channel_iovec_t
条目的实现策略。
在实践中,如果存在大量 zx_channel_iovec_t
,在用户空间中进行线性化可能更高效,或者至少可以避免将工作转移到内核。不过,为了简单起见,建议您先采用 8192
限制,待确定是否需要进一步优化后再做决定。
上限更高的一个实现级后果是,zx_channel_iovec_t
数组无法完全放入内核堆栈中。堆栈缓冲区可用于常见情况,但当条目足够多时,需要将其复制到更大的(速度较慢)缓冲区中。
矢量化手柄
您可以为句柄使用等效的 zx_channel_iovec_t
,也可以将句柄与现有 zx_channel_iovec_t
中的字节一起添加。不过,对于句柄而言,这些优势更为有限,因为句柄数组通常较小。为简单起见,句柄会保留在专用数组中。
支持在单次写入中发送多条消息
此 RFC 的早期版本包含一项提案,旨在支持在单个 zx_channel_write
调用中发送多个消息。
我们考虑了以下三种方案:
- 扁平表示法:使用两个
uint16_t
字段(message_seq
和handle_count
)重新指定zx_channel_iovec_t
上的reserved
字段:message_seq
(zx_channel_iovec_t
所属的消息)和handle_count
(buffer
中的字节消耗的句柄数)。序列号受到限制,必须单调递增且没有间隔。此约束条件可实现性能更高的内核实现,但日后可根据需要予以放宽。此方法与此 RFC 保持一致,并且可以将多消息支持添加到现有结构中。 - 数组-数组表示法:有一个消息外部数组,每个消息都有指向每个消息的 iovec 内部数组的指针。这与 Linux 系统调用
sendmmsg
中使用的结构类似,可能更为用户所熟悉。虽然我们没有衡量数组数组表示法的性能,但有证据表明,由于间接,开销可能会达到 5-25%(请参阅 CL)。 - 带标头前缀的表示法:缓冲区以标头开头,后跟 iovec 数组。该标头由 16 个消息描述符组成,每个描述符仅包含一个
uint16_t
message_size
字段。此字段用于确定与消息关联的zx_channel_iovec_t
条目的数量。这种表示法提供了分层结构,但无需额外的重定向和复制。
在设计讨论中,由于扁平表示法具有性能特性和简单性,因此受到青睐。虽然本 RFC 不涵盖多消息支持的完整提案,但请注意,本 RFC 与扁平表示法兼容。
专用于 iovec 的系统调用
您可以创建 zx_channel_write_iovec
、zx_channel_write_etc_iovec
、zx_channel_call_iovec
和 zx_channel_call_etc_iovec
系统调用,而不是向现有系统调用添加新选项。不过,最好使用选项,以免系统调用次数激增并增加用户的认知负担。
zx_channel_read 中的 zx_channel_iovec_t 支持
此 RFC 提议支持对通道写入使用 zx_channel_iovec_t
,但不支持对通道读取使用 zx_channel_iovec_t
。原因在于,在写入端,使用 iovec 有明确的动机(即避免执行 FIDL 线性化步骤),但在读取端,没有明确且直接的好处。
Rust 绑定可以通过将缓冲区划分为多个各自拥有自己所有权的小缓冲区,从读取端 iovec 支持中获益。这有助于实现类似于 LLCPP 的绑定变体,该变体本质上会将缓冲区转换为输出对象。不过,我们没有短期计划将 Rust 绑定更改为以这种方式运行,而且推迟到需要时才添加对读取路径 iovec 的支持似乎不会带来太多成本。
在先技术和参考文档
Fuchsia 已有使用矢量化 IO 的 zx_stream_readv
和 zx_stream_writev
系统调用。Linux 还提供类似的 readv
和 writev
系统调用,分别用于读取和写入文件描述符。