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

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

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

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

摘要

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

设计初衷

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

原则上,可以使用共享内存来避免部分或全部开销,同时保留 Fuchsia 用户空间隔离模型的优势。但是,原始共享内存存在局限性,这使得在许多情况下(尤其是涉及多个客户端、信任或可靠性程度不同或共享资源的情况)实现安全、高效且可靠的通信变得非常困难。

其中一些挑战包括:

  • 安全性和正确性:直接访问共享内存通信模式很难在不增加显著开销或显著复杂性的情况下,使其能够抵御滥用或滥用行为。重要的是,与任意内存访问相比,系统调用的验证更简单。
  • 同步:除了架构内存模型之外,直接内存访问还提供有限的同步机制。
  • 生命周期和会话:使用现有基元很难推理和强制执行内存访问的范围。

此提案是一种务实的方法,可用于解决这些问题及其他问题,它利用 Zircon 对等对象模型来更好地管理会话,并利用内核作为可信中介来强制执行义务。

利益相关方

教员

  • cpu@google.com

Reviewers:

  • 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 的设计审核和迭代。

设计

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

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

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

与由其他离散对象组成的类似结构相比,这种生命周期管理、内存区域封装、可配置访问控制和内核中介操作的灵活组合支持各种共享内存通信和 IO 模式,并且更加安全和便捷。

目标

此提案的宏观目标如下:

  • 通过减少通信和 I/O 的内存分配、系统调用和调度开销,缩小与单体系统的性能差距。
  • 简化共享内存生命周期管理和同步。
  • 在需要时强制执行强大的安全不变性。
  • 为未来的内核和用户空间接口奠定基础。

端点

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

内存区域

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

访问控制

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

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

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

类型 Ep0 Ep1
映射(用户) 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“对照组”:
    • Ep0:读写映射访问权限。
    • 第 1 集:RO 映射访问权限。
    • 用途:服务器会在此区域中写入原子变量,以向客户端发布状态并协调客户端的活动。
  2. 区域 1“字符串映射”:
    • Ep0:RO 映射访问权限。
    • 第 1 集:中介 WO 访问。
    • 用法:客户端使用内核中介操作将字符串内部化,服务器使用映射无锁地解读这些字符串。由于客户端访问仅限中介,因此客户端不得篡改字符串映射,只能添加新条目,从而消除服务器的潜在检查时间、使用时间危险。
  3. 区域 2“数据环形缓冲区”:
    • Ep0:读写映射访问权限。
    • 第 1 集:读写映射访问。
    • 用法:客户端和服务器都使用商定的协议通过映射直接访问此区域,并接受潜在的完整性问题。

基本 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 用于指定 regions 数组中的元素数量。

  • 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:Ep0 的 kR0 中介访问。
    • 位 3:Ep0 的 kW0 中介访问。
    • 第 4 位:Ep1 的 uR1 映射。
    • 第 5 位:Ep1 的 uW1 映射。在某些系统上,可能暗示 uR1。
    • 位 6:Ep1 的 kR1 中介访问。
    • 位 7:Ep1 的 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;
};
  • 选项,预留以供日后使用。
区域类型 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 是一个输出参数,用于返回映射的虚拟地址。

支持的选项:

  • 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

将 IOB 的每个区域的相关信息作为 zx_iob_region_info_t region description 元素的数组返回。

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

为了与现有内存归因机制保持一致,本主题会像对待常规 VMO 一样对 IOB 区域的后备内存对象进行统计,包括通过句柄和映射的可单独访问性。

对于私有区域类型,系统会为后备内存对象分配不同的 KOID,并且默认情况下,这些对象与拥有的 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 等低级设施的插桩。
  • 通过允许内核安全地回收轨迹事件缓冲区来避免严重的 OOM 情况,从而提高稳健性,同时结合使用 RFC 0181:无锁可丢弃 VMO

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

  • 角色:
    • 第 0 集:Trace Manager
    • 第 1 集:轨迹提供程序
  • 区域 0:轨迹类别位矢量
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • 第 1 集:uR1 (RO)
    • 学科:无
    • 用途:一个 uint64_t 位矢量数组,表示已启用的跟踪类别。
  • 区域 1:轨迹类别
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • 第 1 集:kW1(仅添加中介条目)
    • 纪律:未来的 ID/blob 分配器纪律。
    • 用途:一个顺序 ID/字符串映射,表示跟踪提供程序使用的每个跟踪类别的类别位。
  • 区域 2:内部化字符串
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • 第 1 集:kW1(仅添加中介条目)
    • 纪律:未来的 ID/blob 分配器纪律。
    • 用途:一个顺序 ID/字符串映射,用于表示轨迹事件名称和其他经常引用的名称的内化字符串。
  • 地区 3:低费率 / 静态元数据
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • Ep1:uR1、uW1(读写)
    • 纪律:未来的多生产者/单消费者环形缓冲区纪律。
    • 用途:适用于低速率 / 静态跟踪事件(例如线程名称)的无锁环形缓冲区。通常,需要这些事件才能正确解读高速率缓冲区中的其他轨迹事件。
  • 区域 4:高比特率跟踪事件
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • Ep1:uR1、uW1(读写)
    • 纪律:未来的多生产者/单消费者环形缓冲区纪律。
    • 用途:用于高速跟踪事件的无锁环形缓冲区。在循环缓冲区模式下,对数据丢失具有容错性。

系统日志记录

IOB 旨在通过支持以下功能,为更安全、更高效的系统日志记录奠定基础:

  • 将日志与不同组件和/或严重级别隔离,防止跨组件和跨严重级别的日志滚动。
  • 避免处理未被积极监控的日志。
  • 不再使用调度线程来处理严重性更改。
  • 按组件(内存)的资源管理和问责。

涉及以下参与者:

  1. 日志生成方:发出日志的组件。
  2. 日志管理器(归档程序):将来自所有生产者的日志路由到所有使用方。
  3. 日志使用方:连接到日志管理器并读取合并的日志流。

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

  • 角色:
    • 第 0 集:日志管理器(归档管理器)
    • 第 1 集:日志生产方(某个组件)
  • 区域 0:对照组
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读写)
    • 第 1 集:uR1 (RO)
    • 学科:无
    • 用途:存储运行时配置,例如最低严重程度。还可以存储有关日志循环缓冲区大小上限的信息,具体取决于内存管理要求。
  • 区域 1:字符串表
    • 类型:私享
    • 权限:
    • 第 0 集:uR0(罗马尼亚)
    • 第 1 集:kW1 (WO)
    • 纪律:未来的 ID/blob 分配器纪律。
    • 用途:一个顺序 ID/字符串映射,用于表示经常引用的日志字符串、标记、键、元数据的内化字符串。
  • 区域 2:日志循环缓冲区
    • 类型:私享
    • 权限:
    • Ep0:uR0、uW0(读/写)(针对 R/W/C 指针进行写入,并写入正在处理的计数器)
    • 第 1 集:kW1 (WO)
    • 纪律:未来的多生产者/单消费者环形缓冲区纪律。
    • 用途:用于日志记录的无锁环形缓冲区。在循环缓冲区模式下,对数据丢失具有容错性。计划先从生产者进行内核中介的写入,然后再从管理器进行直接读取。如果性能需要,可能会改为由生产者直接访问。

实现步骤

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

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

性能

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

工效学设计

从设计上讲,与实现类似功能所需的 VMO、事件和 IPC 对象组合相比,IOB 更易于推理。与组合使用多个对象相比,IOB 端点的生命周期也更容易协调地管理,并且与 VMO 相比,内存访问规则更容易强制执行。

向后兼容性

IOB 仅通过向该语言引入新的句柄类型来影响 FIDL 文件源代码兼容性和 ABI 线格格式兼容性,而不会影响向后兼容性。

安全注意事项

共享内存使用场景始终存在一些潜在的安全漏洞。大多数 IOB 用法不会引入共享 VMO 不受影响的新危险。不过,由于关于权限和内存访问生命周期的规则更为严格,IOB 确实可以降低发生某些错误的风险。

将“复制省略”和“检查时间”更改为“使用时间”危险

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

在开放式共享内存用例中避免 TOCTOU 危险可能具有挑战性,通常需要将数据从共享内存区域复制出去,以避免在验证期间或验证后进行修改。即使采用了复制,也必须小心防范复制省略,因为除非使用特定的干预措施,否则编译器可能会优化掉复制操作,并直接对共享内存区域进行操作。

内核中介的操作可以通过保证内核遵循元数据更新、载荷传输和访问内存顺序的规则,简化 TOCTOU 危险的缓解。例如,使用环形缓冲区纪律时,在使用方通过更新环形缓冲区记账确认可以覆盖记录内容之前,内核保证不会覆盖记录内容,从而避免在验证之前复制载荷。

尽管如此,用户仍需负责正确应用内存有序性和原子操作,或语言专用等效项,以防止因优化而导致正确性风险。为了减轻用户的负担,Fuchsia SDK 将包含适用于受支持语言的库,这些库会为每个定义的学科实现正确的访问例程。违规处置规范还将包含足够的详细信息,以便安全且正确地应用访问权限规则。

隐私注意事项

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

测试

测试包括以下内容:

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

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

文档

文档更新包括:

  • 系统调用引用。
  • 内核概念。
  • FIDL 句柄类型。

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

由于该功能基于虚拟内存和对等调度程序的现有机制,因此实现此提案的成本相对较小。这些独特功能以非对称访问权限、内核中介区域访问和关闭时取消映射功能为中心。

在先技术和参考文档