| 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 绑定,以使用 zx_channel_iovec_t。预计这对 FIDL 用户的影响微乎其微。
性能
我们实现了原型并进行了基准测试。
此原型在内核端实现了 zx_channel_write 选项,并提供了有限的 FIDL 支持(仅支持内联对象和向量)。消息头以及每个内嵌对象和非内嵌对象都具有 zx_channel_iovec_t 条目。一个包含 64 个条目的数组用于在内核和 FIDL 编码中存储 zx_channel_iovec_t。
这些测量结果来自配备 Intel Core i5-7300U CPU @ 2.60GHz 的机器。
字节向量事件基准(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的数量应设置下限,可能更接近 16 而不是 8192。这样一来,zx_channel_iovec_t 数组便可复制到内核的堆栈上。不过,这会妨碍为每个非内联 FIDL 对象分配一个条目的实现策略。zx_channel_iovec_t
实际上,当 zx_channel_iovec_t 的数量较多时,在用户空间中进行线性化可能更高效,或者至少可以避免将工作转移到内核。不过,在确定是否需要进一步细化之前,建议将 8192 限制设为 100,以简化操作。
更高级别的限制在实现方面带来的一个后果是,zx_channel_iovec_t 数组无法完全容纳在内核堆栈中。堆栈缓冲区可用于常见情况,但当条目足够多时,需要将其复制到更大的(但速度较慢的)缓冲区中。
向量化句柄
可以为句柄提供等效于 zx_channel_iovec_t 的功能,或者将句柄与字节一起包含在现有的 zx_channel_iovec_t 中。不过,对于句柄,这种优化带来的好处较为有限,因为句柄数组往往很小。为简单起见,句柄保留在专用数组中。
支持在单次写入中包含多条消息
此 RFC 的先前版本包含一项提案,建议支持在单个 zx_channel_write 调用中包含多条消息。
我们考虑了以下三项提案:
- 扁平表示法:重新利用
zx_channel_iovec_t上的reserved字段,并使用两个uint16_t字段:message_seq(zx_channel_iovec_t所属的消息)和handle_count(buffer中字节所消耗的句柄数)。序列号必须单调递增且没有间隙。此限制可实现性能更高的内核,但未来可根据需要放宽。此方法与此 RFC 一致,并且可以在现有结构中添加多消息支持。 - 数组的数组表示法:有一个外部消息数组,每个消息都包含指向内部 iovec 数组的指针。这与 Linux 系统调用
sendmmsg中使用的结构类似,用户可能更熟悉。虽然我们没有测量数组的数组表示法的性能,但有证据表明,由于间接寻址,可能会产生 5-25% 的开销(请参阅 CL)。 - 以标头为前缀的表示形式:缓冲区以标头开头,后跟 iovec 数组。标头包含 16 个消息描述符,每个描述符仅包含一个
uint16_tmessage_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,但不支持通道读取。这是因为写入侧的 iovec 有明确的动机(避免 FIDL 线性化步骤),但读取侧没有明确的直接好处。
通过将缓冲区划分为多个较小的缓冲区(每个缓冲区都有自己的所有权),Rust 绑定可能会受益于读取端 iovec 支持。这将有助于实现一种类似于 LLCPP 的绑定变体,该变体基本上会将缓冲区转换为输出对象。不过,目前还没有短期计划要更改 Rust 绑定以实现此目的,而且在需要之前推迟添加对读取路径 iovec 的支持似乎不会带来太多成本。
在先技术和参考资料
Fuchsia 具有使用矢量化 IO 的现有 zx_stream_readv 和 zx_stream_writev 系统调用。Linux 还提供了类似的 readv 和 writev 系统调用,分别用于读取和写入文件描述符。