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
的错误条件,其中包含由于引入 iovecs 而进行的更新。
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
条目。
实现
系统调用
- 引入了设计部分中定义的
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 绑定可以添加对将 FIDL 对象编码为 zx_channel_iovec_t
数组的支持,从而选择性地利用 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
条目。一个由 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 ns -> 88 ns
- 256 字节矢量:251 ns -> 86 ns
内嵌对象也在编码方面实现了细微的改进:
- 包含 256 个 uint8 字段的结构体:67 ns -> 49 ns
安全注意事项
鉴于这是对现有系统调用的重大变更,在实现之前需要进行安全审核。
隐私注意事项
这不会对隐私权造成影响。
测试
对于每个发生变化的层,系统将添加单元测试和集成测试。
我们不打算添加设备或系统范围的端到端测试,但现有的测试范围将有助于确保在迁移完成后不会引入任何意外 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
调用中使用多条消息的方案。
其中考虑了以下三项建议:
- 平面表示:将
reserved
字段重新调整为使用两个uint16_t
字段:message_seq
(zx_channel_iovec_t
所属的消息)和handle_count
(buffer
中字节使用的句柄数量)。序列号被限制为单调,没有间隙。zx_channel_iovec_t
此约束条件可实现性能更高的内核实现,但将来如果需要,这个限制可能会降低。此方法符合此 RFC,可向现有结构添加多消息支持。 - 数组数组表示法:存在一个外部消息数组,其中每个数组都有指向每条消息的内部 iovecs 数组的指针。它与 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
进行通道写入,但不支持通道读取。原因在于,在写入端使用 iovecs 有明确的动机,那就是避免执行 FIDL 线性化步骤,但读取端没有明确而立竿见影的好处。
通过将缓冲区划分为多个较小的缓冲区,每个缓冲区具有自己的所有权,Rust 绑定可能受益于读取端 iovec 支持。这将有助于实现类似于 LLCPP 的绑定变体,后者本质上会将缓冲区转换为输出对象。不过,目前没有改变 Rust 绑定以使其以这种方式运作的短期计划,并且等到需要时再延迟添加对读取路径 iovec 的支持似乎不会产生太大的成本。
早期技术和参考资料
Fuchsia 现有使用矢量化 io 的 zx_stream_readv
和 zx_stream_writev
系统调用。Linux 还提供了类似的 readv
和 writev
系统调用,分别用于读取和写入文件描述符。