RFC-0218:IOBuffer:用于高效 IO 的对等互连共享内存对象

RFC-0218:IOBuffer:用于高效 IO 的对等共享内存对象
状态已接受
领域
  • 内核
  • 系统
说明

引入了新的共享内存对象,旨在改进分布式 IO 用例。

问题
Gerrit 更改
  • 619562
作者
审核人
提交日期(年-月-日)2022-09-29
审核日期(年-月-日)2023-05-09

总结

我们提出了新的 IOBuffer 内核基元,以改进通信和分布式 IO 用例的安全性、便利性和性能,它使用共享内存和可选的内核中介操作。

设计初衷

Fuchsia 操作系统的大部分内容都是在用户空间中实现的,并分布在多个通信进程和组件中。与单体式系统中的等效实现相比,这种有意识的设计选择通常需要更多的进程和线程,尤其是当等效功能直接在单体内核中实现时。虽然这种分布式方法在安全性、开发和更新性方面有明显的优势,但重要的是,在 Fuchsia 中,许多操作都需要 IPC,而单体内核中只需要系统调用和状态读/写操作。此外,使用现有 Fuchsia 通道和套接字基元的 IPC 涉及多个系统调用、模式切换、线程跃点和数据复制,以完成端到端操作。此开销的影响可能对通信和 IO 工作负载产生重大影响,尤其是涉及大量、高频率和/或低延迟操作的工作负载。

原则上,可以使用共享内存来避免部分或全部此类开销,同时保留 Fuchsia 用户空间隔离模型的优势。然而,原始共享内存具有一些局限性,使得在许多情况下安全、高效且可靠的通信,尤其是当涉及多个客户端、信任或可靠性程度不同或者共享资源时。

一些挑战包括:

  • 安全和正确性:直接访问共享内存通信模式很难使攻击者能够抵御滥用或滥用,而不会产生大量开销或复杂性。重要的是,与任意内存访问相比,系统调用更易于验证。
  • 同步:直接内存访问可以提供超出架构内存模型所需的有限同步机制。
  • 生命周期和会话:内存访问权限的范围可能难以使用现有基元来推断和强制执行。

该方案是一种应对这些挑战和其他挑战的实用方法,利用 Zircon 对等互连对象模型更好地管理会话,并使用内核作为可信中介来履行义务。

利益相关方

教员

  • cpu@google.com

审核者

  • adanis@google.com
  • rashaeqbal@google.com
  • fmeawad@google.com
  • gmtr@google.com
  • maniscalco@google.com
  • johngro@google.com
  • abarth@google.com
  • cpu@google.com

咨询人员

  • miguelfrde@google.com
  • puneetha@google.com
  • mseaborn@google.com
  • mcgrathr@google.com
  • quiche@google.com
  • mvanotti@google.com
  • brunodalbo@google.com

社交

RFC 对 Fuchsia 性能、诊断和 Zircon 性能工具 WG 进行了设计审核和迭代。

设计

IOB (IOB) 设计引入了一种新型内存对象,用于高效通信和分布式 IO。新对象将具有不同属性和角色的多个内存区域封装到单个连贯实体中,具有实用的生命周期管理、访问权限控制和内核参与的操作。

IOB 是具有两个端点的对等互连对象对。端点提供与其他对等互连对象(例如通道、套接字、fifos 和事件对)类似的生命周期管理和信号传输。除了常规的端点句柄权限提供的访问权限控制之外,还可以针对每个端点单独配置对每个内存区域的访问权限,以支持精细的安全属性。

可配置的内核参与的操作支持更强大的安全属性,其中内存访问通过内核系统调用代表用户执行,而内核则充当可信中介,始终遵循所请求的访问规则。参与中介的操作需要一定的开销来换取稳健性和安全性,同时保持比其他通信基元更低的端到端开销。

生命周期管理、内存区域封装、可配置的访问控制以及由内核参与的操作的这种灵活组合支持各种共享内存通信和 IO 模式,与其他由其他离散对象组成的类似结构相比,这种方法具有更高的安全性和便利性。

目标

此提案具有以下总体目标:

  • 通过减少内存分配、系统调用以及通信和 IO 的调度开销,缩小与单体式系统的性能差距。
  • 简化共享内存生命周期管理和同步。
  • 根据需要强制实施强安全不变的机制。
  • 为未来的内核和用户空间接口提供基础。

端点

每个 IOB 都有两个端点:Ep0 和 Ep1。IOB 端点句柄可以由多个进程复制和共享。只要存在至少一个端点句柄,IOB 就会保留底层内存对象,以便为其他对等互连对象提供类似的生命周期语义。

内存区域

IOB 封装一个或多个具有独立属性和访问权限控制的内存区域的集合,以支持每个区域在通信协议中的预期角色。例如,不同的区域可能会用于实现键值对存储、客户端协调、状态管理和数据载荷传输等用途。

访问权限控制

在 IOB 创建期间,系统会按端点和区域组合配置内存访问。可以为每个端点授予每个区域的不同访问权限。验证操作时,区域访问权限控制会与端点的句柄权限结合起来。操作的有效权限可能比标识名权限更严格,但不能超过标识名权限许可。

内存访问也可以通过内核进行中介,而无需映射。

下表列出了与区域相关的三种访问权限控制类型。映射(用户)和参与中介(内核)的访问权限控制可以为每个区域单独设置,而端点权限来自启动操作的句柄。

类型 第 0 集 第 1 集
映射(用户) uR0、uW0 uR1、uW1
参与中介的(内核) kR0、kW0 kR1、kW1
端点(句柄) hR0、hW0 hR1、hW1

端点 Epn 的映射或中介操作的有效访问权限(eRn 和 eWn)的计算方式如下:

操作 读取权限 (eRn) 写入权限 (eWn)
地图 uRn 和 hRn uWn 和 hWn
参与中介的操作 kRn 和 hRn kWn 和 hWn

参与中介的访问权限控制和方向

用户和内核访问权限控制之间的细微区别在于,前者在绝对意义上控制读取或写入访问,而后者在逻辑或方向上运行。例如,内核参与的读取操作可能涉及更新某个区域中的簿记数据结构,以指明从缓冲区读取的数据量。这种意义上的只读访问不会阻止内核更新簿记,而是表示只允许逻辑读取操作和相关的簿记更新。

示例

以下示意图展示了具有三个具有逻辑函数的内存区域的 IOB。端点 Ep0 和 Ep1 在逻辑上分别分配给服务器角色和客户端角色。

  1. 区域 0“Control”:
    • 第 0 集:RW 映射访问。
    • 第 1 集:RO 映射访问权限。
    • 用法:服务器在此区域中写入原子变量,以将状态发布到客户端,并协调客户端的活动。
  2. 区域 1“String Map”:
    • 第 0 集:RO 映射访问权限。
    • 第 1 集:参与中介的 WO 访问。
    • 用法:客户端使用内核参与的运算来内部化字符串,服务器使用映射对这些字符串进行无锁解释。由于客户端访问仅限中介,客户端不得篡改字符串映射,但必须添加新条目,从而消除服务器可能面临的检查时间和使用时间方面的危险。
  3. 区域 2“数据环缓冲区”:
    • 第 0 集:RW 映射访问。
    • 第 1 集:RW 映射访问权限。
    • 用法:客户端和服务器均使用商定的协议直接通过映射访问此区域,并接受潜在的完整性问题。

基本 IO 环形缓冲区

恣意创作

通过使用一组选项调用 zx_iob_create 即可创建 IOB。

zx_status_t zx_iob_create(uint64_t options,
                          const zx_iob_region_t* regions,
                          uint32_t region_count,
                          zx_handle_t* ep0_out,
                          zx_handle_t* ep1_out);

参数

  • options 预留以供日后使用。

  • regions 是指向 zx_iob_region_t 区域描述数组的指针。

  • region_count 指定区域数组中的元素数量。

  • ep0_outep1_out 是 IOB 端点的初始句柄的输出参数。

区域说明

区域的几何图形和配置由 zx_iob_region_t 区域说明结构指定。基本结构包括所有区域类型通用的字段。

struct zx_iob_region_t {
  uint32_t type;
  uint32_t access;
  uint64_t size;
  zx_iob_discipline_t discipline;
  union {
    zx_iob_region_private_t private_region;
    uint8_t max_extension[4 * 8];
  };
};
  • type 用于指定支持该区域的区域和内存对象的类型。

    • 类型 0:专用区域
    • 类型 1+:预留以供日后使用。
  • access 会为每个端点指定访问权限控制修饰符。内存访问规则可以指定哪些访问位的组合可用于支持安全性或正确性属性。

    • 位 0:Ep0 的 uR0 映射。
    • 位 1:Ep0 的 uW0 映射。在某些系统上,可能意味着 uR0。
    • 位 2:kR0 对 Ep0 的中介访问。
    • 位 3:针对 Ep0 的 kW0 中介访问。
    • 位 4:第 1 集的 uR1 映射。
    • 位 5:第 1 集的 uW1 映射。在某些系统上,这可能表示 uR1。
    • 位 6:kR1 对第 1 集进行中介访问。
    • 位 7:针对第 1 集的 kW1 中介访问。
    • [8..31]:预留以供日后使用。
  • size 是请求的区域大小(以字节为单位)。大小将向上舍入为系统页面大小的边界。结合使用 zx_object_get_info 与主题 ZX_INFO_IOB_REGIONS 来确定区域的实际大小。

  • discipline 指定要用于内核中介型操作的内存访问规则。

区域类型 0:专用

指定由 IOB 唯一拥有的私有内存对象支持的区域。只能通过对自身拥有的 IOB 执行的操作和映射此内存对象来访问此内存对象。

struct zx_iob_region_private_t {
  uint64_t options;
};
  • options 预留以供日后使用。
区域类型 1+:预留以供日后使用

未来的扩展可能包括共享区域类型,其中同一内存对象由多个 IOB 共享,以支持高效的多代理协调。

映射

IOB 的内存区域通过调用 zx_vmar_map_iob 进行映射。此调用的语义与 zx_vmar_map 类似。

zx_status_t zx_vmar_map_iob(zx_handle_t vmar,
                            zx_vm_option_t options,
                            size_t vmar_offset,
                            zx_handle_t ep,
                            uint32_t region_index,
                            uint64_t region_offset,
                            size_t region_len,
                            zx_vaddr_t* addr_out);

参数

  • vmar 是要将区域映射到的 VMAR 的句柄。
  • options 等效于 zx_vmar_mapoptions 参数。仅支持下列选项。
  • vmar_offset 等同于 zx_vmar_mapvmar_offset 参数。
  • ep 是包含要映射的区域的端点。
  • region_index 是要映射的内存区域的索引。
  • region_offset 等同于 zx_vmar_mapvmo_offset 参数。
  • region_len 等同于 zx_vmar_maplen 参数。
  • addr_out 是返回映射的虚拟地址的 out 参数。

支持的选项:

  • ZX_VM_SPECIFIC
  • ZX_VM_SPECIFIC_OVERWRITE
  • ZX_VM_OFFSET_IS_UPPER_LIMIT
  • ZX_VM_PERM_READ
  • ZX_VM_PERM_WRITE
  • ZX_VM_MAP_RANGE

其他预留供将来使用的选项,如果指定,则返回 ZX_ERR_INVALID_ARGS

映射专用区域在功能上与映射不可调整大小的 VMO 相同。如果映射超出内存对象的末尾,并且未设置 ZX_VM_ALLOW_FAULTS,则返回 ZX_ERR_BUFFER_TOO_SMALL

包含 region_offsetregion_len 参数是映射 API 的最佳做法。采用此最佳实践的动机包括:

  • 支持拦截映射调用的排错程序。使用显式范围可以简化对主动映射区域的跟踪。
  • 支持可明确管理地址空间放置和区域覆盖的沙盒。
  • 更笼统地说,就是在使用 ZX_VM_SPECIFIC_OVERWRITE 时避免意外碰撞。

VMAR 操作

映射的区域通常支持 VMAR 操作,例如 zx_vmar_protectzx_vmar_op_rangezx_vmar_destroy。区域的访问权限规则可以按照规则规范中所述修改或限制这些操作。

对象信息查询

IOB 支持通过 zx_object_get_info 查询对象属性和内存区域。返回的详细信息包括支持每个区域(用于验证和安全目的)的内存对象的关键属性。

主题 ZX_INFO_IOB

返回有关整个 IOB 实例的信息。

struct zx_iob_info_t {
  uint64_t options;
  uint32_t region_count;
  uint8_t padding1[4];
};
成员
  • options 是传递给 zx_iob_createoptions 参数的值。
  • region_count 是 IOB 中的区域数量。

主题 ZX_INFO_IOB_REGIONS

zx_iob_region_info_t 区域说明元素数组的形式返回有关 IOB 的每个区域的信息。

struct zx_iob_region_info_t {
  zx_iob_region_t region;
  zx_koid_t koid;
};
  • region 是区域描述,包含可能交换的访问位。
  • koid 是底层内存对象的 koid。

访问修饰符位会进行交换,以便 Ep0 访问位反映进行查询的端点的访问,而 Ep1 位反映对另一个端点的访问,这样一来,创建本地和远程句柄时就无需知道句柄是 Ep0 和 Ep1 时的访问权限。

主题 ZX_INFO_PROCESS_VMOS

为了保持现有内存归因机制的连续性,支持 IOB 区域的内存对象像常规 VMO 一样按此主题进行计数,包括句柄和映射的可达性。

对于专用区域类型,后备内存对象被分配了不同的 KOID,并且默认与所属 IOB 具有相同的名称。访问规则可以替换或修改专用区域的默认名称。

参与中介的访问

IOB 支持通过可配置的内核参与对内存区域的访问。通过将内核用作可信中间层,参与中介访问可提供更严格的数据完整性和其他不变量,要比直接访问更加严格。内核会保证遵循访问规则为内存区域指定的访问规则。

参与中介的读取、写入等操作...

中介操作由 IOB 读取、写入和辅助系统调用执行,将在未来的扩展程序中指定。只要使用方式在语义上一致,访问规则就可以定义新的 IOB 系统调用或使之前定义的 IOB 系统调用重载。一般目标是定义一些适合许多用例的可重用系统调用。

权限

IOB 默认情况下具有以下权限:

  • ZX_RIGHTS_BASIC = (ZX_RIGHT_TRANSFER | ZX_RIGHT_DUPLICATE | ZX_RIGHT_WAIT | ZX_RIGHT_INSPECT)
  • ZX_RIGHTS_IO = (ZX_RIGHT_READ | ZX_RIGHT_WRITE)
  • ZX_RIGHTS_PROPERTY
  • ZX_RIGHTS_MAP
  • ZX_RIGHTS_SIGNAL
  • ZX_RIGHTS_SIGNAL_PEER

属性

IOB 支持 ZX_PROP_NAME 以提供诊断和归因信息。

IOB 可以支持区域访问规则定义的其他属性。

信号

IOB 支持对等方和用户信号,以提醒用户注意值得注意的条件。未来的扩展程序可能会定义可配置的信号。

ZX_IOB_PEER_CLOSED

当最后一个对其他端点的引用被释放时,系统会在该端点上引发 ZX_IOB_PEER_CLOSED。端点的句柄和从该端点创建的映射会计为对此目的的引用。

ZX_USER_SIGNAL_*

用户可以抛出 ZX_USER_SIGNAL_* 来指示值得注意的条件。

内核参与的访问规则

为了方便参与中介的操作,内核必须知道如何正确访问内存区域。对区域进行内存访问的规则和配置称为访问规则,其中可能包括:

  • 账簿的位置和格式。
  • 覆盖/失败政策。
  • 水印和超时设置。
  • 内存顺序和栅栏要求。

规则说明

访问学科使用可扩展的说明结构指定,其中 32 位标头用于指定说明的类型和格式。

#define ZX_IOB_DISCIPLINE_TYPE_NONE (0)

struct zx_iob_discipline_t {
  uint32_t type;
  union {
    uint8_t max_extension[4 * 15];
  };
};

未来 RFC 中可能会定义的学科示例包括:

  • 生产方 / 消费方环形缓冲区
  • ID / Blob 映射
  • 键 / 值存储
  • 分散 / 收集内存副本
  • 目录条目缓存

实现

初始实现是支持跟踪和日志记录客户的基础步骤。下面的说明是案例研究,说明了定义 IOB 功能的动机,以及满足用例要求的潜在扩展。

系统事件跟踪

IOB 旨在通过支持以下各项,为更安全、更高效的系统事件跟踪奠定基础:

  • 通过将会话协调、元数据和事件收集专门移至 IOB 区域,降低运行时开销,在初始设置后便无需调度线程和通道 IPC。
  • 通过消除 IOB 内核 API 以外的依赖项,实现 FDIO 和 FIDL 等低级别工具的插桩。
  • 通过结合使用 RFC 0181:无锁式舍弃 VMO,允许内核安全地收回跟踪事件缓冲区,以避免严重的 OOM 条件,从而提高稳健性。

系统跟踪 IOB 可以使用如下配置:

  • 角色:
    • 第 0 集:跟踪管理器
    • 第 1 集:跟踪提供程序
  • 区域 0:轨迹类别位矢量
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:uR1 (RO)
    • 规则:无
    • 用途:uint64_t 位矢量数组,表示已启用的跟踪类别。
  • 区域 1:跟踪类别
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:kW1(仅限参与中介的添加条目)
    • 规则:未来的 ID/blob 分配器规则。
    • 用法:表示跟踪记录提供程序使用的每个跟踪类别的类别位的顺序 ID/字符串映射。
  • 区域 2:内部化字符串
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:kW1(仅限参与中介的添加条目)
    • 规则:未来的 ID/blob 分配器规则。
    • 用法:顺序 ID/字符串映射,表示跟踪事件名称和其他经常引用的名称的内部化字符串。
  • 区域 3:低速率 / 静态元数据
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:uR1、uW1 (R/W)
    • 规则:未来的多生产方/单使用方环形缓冲区规则。
    • 用途:适用于低速率 / 静态跟踪事件(例如线程名称)的无锁环形缓冲区。通常需要这些事件才能正确解释高速率缓冲区中的其他跟踪事件。
  • 区域 4:高速率跟踪事件
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:uR1、uW1 (R/W)
    • 规则:未来的多生产方/单使用方环形缓冲区规则。
    • 用途:适用于高速率跟踪事件的无锁环形缓冲区。能在循环缓冲区模式下容忍数据丢失。

系统日志记录

IOB 通过支持以下方式,为实现更安全、更高效的系统日志记录奠定基础:

  • 将日志与不同组件和/或严重级别隔离开来,从而防止跨组件和跨严重程度的日志滚动。
  • 避免处理无法主动观察的日志。
  • 不再使用调度线程来处理严重级别变化。
  • 按组件(内存)资源管理和问责制。

涉及的操作包括:

  1. 日志提供方:发出日志的组件。
  2. 日志管理器(存档者):将日志从所有生产方路由到所有使用方。
  3. 日志使用方:连接到日志管理器并读取合并的日志流。

系统日志记录 IOB 可以使用如下配置:

  • 角色:
    • 第 0 集:日志管理器 (Archivist)
    • 第 1 集:日志生产方(某些组件)
  • 区域 0:控制
    • 类型:私享
    • 权限:
    • 第 0 集:uR0、uW0 (R/W)
    • 第 1 集:uR1 (RO)
    • 规则:无
    • 用法:存储运行时配置,例如最低严重级别。还可以存储有关日志循环缓冲区大小上限的信息,具体取决于内存管理要求。
  • 区域 1:字符串表
    • 类型:私享
    • 权限:
    • 第 0 集:uR0 (RO)
    • 第 1 集:kW1 (WO)
    • 规则:未来的 ID/blob 分配器规则。
    • 用法:顺序 ID/字符串映射,表示频繁引用的日志字符串、标记、键和元数据的内部化字符串。
  • 区域 2:记录环形缓冲区
    • 类型:私享
    • 权限:
    • ep0:uR0、uW0 (R/W)(针对 R/W/C 指针和运行中的写入计数器的写入)
    • 第 1 集:kW1 (WO)
    • 规则:未来的多生产方/单使用方环形缓冲区规则。
    • 用途:用于日志记录的无锁环形缓冲区。可容忍在环形缓冲区模式下的数据丢失。我们计划从生产方进行内核冥想写入,从管理器直接读取。如果性能需要,可以转而由提供方直接访问。

实施步骤

实现过程将遵循一系列开发步骤:

  1. 在 @next 属性下引入 IOB 接口:
    • 实现基本 IOB 调度程序。
    • 实现系统调用和通用验证。
  2. 对基于 IOB 的跟踪和日志记录,以及分支中必要的 IOB 扩展进行原型设计。
  3. 为必要的扩展程序编写和批准 RFC。
  4. 实现扩展并移除 @next 属性。
  5. 在 IOB 上对 IOB 进行 rebase 跟踪和日志记录,软性地将现有客户端传输到新接口。

性能

在废弃和移除当前的跟踪/日志记录实现之前,可以通过比较类似操作的端到端延迟时间和开销来验证性能改进。持续进行的基准测试可能包括缓冲区内存用量(虚拟机开销)、针对密钥操作的延迟/并发测试,以及用于验证稳定状态内存消耗属性的分配测试。

工效学设计

从设计上来说,IOB 比估算类似功能所需的 VMO、事件和 IPC 对象组合更容易推断。此外,与同时使用多个对象相比,IOB 端点的生命周期也更易于统一管理,并且与内存访问的规则相比,与 VMO 相比更易于强制执行。

向后兼容性

IOB 通过向语言引入新的句柄类型,只会影响 FIDL 文件源兼容性和 ABI 传输格式兼容性,而不会影响向后兼容性。

安全注意事项

共享内存用例总是存在一些安全漏洞的可能性。大多数 IOB 使用不会引入共享 VMO 不会受到的新危险。但是,由于对权限和内存访问生命周期有更严格的规则,因此 IOB 确实可以降低某些错误的风险。

复制省略和检查时间到使用时间危险

在共享内存用例中出现的一类重要软件 bug 是所谓的检查时间到使用时间危险 (TOCTOU)。TOCTOU 危险是由检查某种状态和使用检查结果之间的竞态条件导致的,因此该状态可能会在检查和使用结果之间失效。

在开放式共享内存用例中避免 TOCTOU 危险可能非常困难,通常需要从共享内存区域复制数据,以避免在验证期间或之后进行修改。即使采用复制的方式,也必须小心谨慎,以防止复制省略,因为除非使用特定干预,否则编译器可能会优化副本并直接在共享内存区域上运行。

由内核参与的操作可以确保内核遵循元数据更新、载荷传输和访问内存顺序方面的规则,从而简化缓解 TOCTOU 危害。例如,在使用环形缓冲区规则时,可以保证内核不会覆盖记录的内容,直到使用方通过更新环形缓冲区簿记来确认此操作是安全的操作,因此无需在验证之前复制载荷。

尽管如此,用户仍负责正确应用内存顺序和原子或特定于语言的等效项,以防止因优化而危害正确性。为了减轻用户的负担,Fuchsia SDK 将包含受支持语言的库,这些库为每个定义的学科实现正确的访问例程。纪律规范还将包含足够的详细信息,以便安全正确应用访问规则。

隐私注意事项

除了已适用于 IPC 和共享内存用例的隐私权注意事项之外,IOB 不会引入新的隐私注意事项。系统事件跟踪和日志记录的初始用例将继续承担相同的隐私保护义务。

测试

测试包括以下内容:

  • IOB 的核心测试。
  • 用于对新类型进行句柄类型验证的 FIDL 一致性测试。
  • 扩展了 Fuchsia Tracing System 测试,添加了 IOB 专属功能。
  • 扩展了系统日志记录测试,以包含特定于 IOB 的功能。

不依赖于底层实现详情的现有跟踪和日志记录测试应继续正常运行并通过。

文档

文档更新包括:

  • Syscall 引用。
  • 内核概念。
  • FIDL 句柄类型。

缺点、替代方案和未知情况

实现此方案的费用相对较低,因为该功能基于现有机器构建,用于虚拟内存和对等互连调度程序。其独特功能主要体现为非对称访问权限、内核中介区域访问权限和关闭时取消映射功能。

早期技术和参考资料