RFC-0238:VMO 大小

RFC-0238:VMO 大小
状态已接受
区域
  • 内核
说明

定义 VMO 的大小和流大小。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2024-01-10
审核日期(年-月-日)2024-01-10

摘要

之前,Zircon 会为每个 VMO 关联两个不同的大小:VMO 的大小(以页面为粒度)和 VMO 的内容大小(以字节为粒度)。此 RFC 使 Zircon 系统接口中对这两个大小的处理合理化。

设计初衷

Zircon 系统接口中与 VMO 相关的部分在处理 VMO 大小内容大小方面存在不一致之处。这种不一致性是这些接口发展演变的结果。在不同的时间点,我们尝试以字节粒度、页面粒度以及(在提出此 RFC 之前)以字节和页面粒度来表示每个 VMO 的大小。这种不一致性使得 Zircon 系统接口难以发展,例如难以添加更复杂的分页接口。

利益相关方

哪些人会受到此 RFC 是否被接受的影响?(此部分为可选,但建议填写。)

辅导员

  • hjfreyer@google.com

审核者

  • cpu@google.com
  • csuter@google.com
  • jamesr@google.com
  • rashaeqbal@google.com

已咨询

  • adanis@google.com
  • dworsham@google.com
  • sagebarreda@google.com

共同化

此 RFC 中的提案源自本地存储、虚拟内存管理器和架构团队之间的一系列讨论。在考虑了各种方法后,该工作组决定提议对 Zircon 系统接口进行一些相当适度的更改,如下所述。

要求

  • 进行这些更改后,系统必须继续正常运行。例如,文件必须具有字节级长度,因为大量代码依赖于字节级的文件长度。
  • 该设计必须能够在内核和硬件中高效实现。例如,内存映射必须以页面粒度进行,因为这是硬件支持的。
  • 设计应尽可能减少对内核或 SDK 库的任何公共接口的更改。例如,FDIO 具有一个公共接口,可将 VMO 转换为文件描述符,而无需任何其他上下文信息,这些信息应予以保留。不过,如果迁移客户端是可行的,则允许对公共接口进行小幅更改。
  • 设计应尽可能简单。虚拟内存管理器已经是系统复杂性的一个重要来源。每当向系统的这一部分添加更多复杂性时,我们都需要谨慎行事。

本文档中的关键字“必须”“不得”“必需”“会”“不会”“应”“不应”“建议”“可以”和“可选”应按照 IETF RFC 2119 中的描述进行解释。

设计

从概念上讲,VMO 是一个稀疏的内存页集合。VMO 在整个系统中用于多种用途,包括存储和共享字节级数据。VMO 有多种类型,具体取决于 VMO 中存储的内存类型。例如,VMO 可能包含由分页程序或物理内存支持的虚拟内存,这与硬件资源的关系更为具体。

VMO 具有大小,表示可以存储在 VMO 中的最大内存量。此大小始终是页面大小的偶数倍,因为 VMO 以页面粒度存储数据。客户端可以使用 zx_vmo_get_size 函数了解 VMO 的大小。如果 VMO 是使用 ZX_VMO_RESIZABLE 选项创建的,则客户端可以使用 zx_vmo_set_size 函数更改 VMO 的大小,前提是它们拥有具有 ZX_RIGHT_RESIZE 的句柄。绝大多数 VMO 操作都与此大小相关。

VMO 还有流大小,表示存储在 VMO 中的数据流(如果有)的字节数。VMO 创建时,流大小会进行初始化,通常基于创建 VMO 的函数的实参,并且不一定是页面大小的偶数倍。数据流大小永远不会大于 VMO 大小,因为存储在 VMO 中的数据流不能大于 VMO 中可存储的最大内存量。客户端可以使用 zx_vmo_get_steam_size 函数了解 VMO 的流大小。如果 VMO 适合存储数据流,客户端可以使用 zx_vmo_set_stream_size 函数更改 VMO 的数据流大小,前提是它们具有 ZX_RIGHT_WRITE 句柄。zx_vmo_set_size 函数不会修改流大小,只会强制执行流大小永远不会大于 VMO 本身大小的不变性。通常,客户端通过与 VMO 关联的流对象上的 zx_stream_* 函数与流大小进行交互。与同一 VMO 关联的所有视频流对象共享视频流大小。

ZX_VMO_UNBOUNDED

当客户端使用 zx_vmo_create 创建 VMO 时,size 实参用于初始化 VMO 的大小和流大小。VMO 的大小会初始化为向上舍入到最接近的页面边界的大小。VMO 的流大小初始化为 size,不进行任何舍入。

此 RFC 为 zx_vmo_createzx_pager_create_vmo 引入了 ZX_VMO_UNBOUNDED 选项,用于创建大尺寸的 VMO。如果客户端提供此选项,则 VMO 的大小会初始化为最大可能值,而不是根据 size 实参进行初始化。ZX_VMO_UNBOUNDED 选项适用于 VMO 大小没有实际用途的情况,例如由分页器支持的文件系统使用的 VMO 和支持 C 运行时堆的 VMO。因此,ZX_VMO_UNBOUNDED 不能与 ZX_VMO_RESIZABLE 选项搭配使用。

ZX_VM_FAULT_BEYOND_STREAM_SIZE

此 RFC 为 zx_vmar_map 引入了 ZX_VM_FAULT_BEYOND_STREAM_SIZE 选项。如果客户端提供此选项,则对映射(超出 VMO 中包含数据流的最后一页)的内存访问将出现故障。该映射仍可用于读取和写入数据流之外的内存,但只能写入到下一个页面边界,因为内存映射以页面粒度存在。当具有此类映射的 VMO 调整大小时,Zircon 会移除数据流之外的页面的页表条目,以便将来对这些页面的访问会生成故障。

ZX_VM_FAULT_BEYOND_STREAM_SIZE 选项有助于实现与类 POSIX 操作系统(例如 Linux)相匹配的 mmap 语义。Linux 和 OpenBSD)。在这些操作系统中,超出与内存映射文件内容重叠的最后一页的内存访问会生成故障。

脏范围

未来针对 zx_pager_query_dirty_ranges 的 RFC 可能会添加一个选项,以支持在 VMO 的数据流中查询脏范围,而不是像当前默认那样在整个 VMO 中查询脏范围。

ZX_PROP_VMO_CONTENT_SIZE

此 RFC 弃用ZX_PROP_VMO_CONTENT_SIZE。客户端应改用 zx_vmo_get_stream_sizezx_vmo_set_stream_size。在可预见的未来,Zircon 将保留对 ZX_PROP_VMO_CONTENT_SIZE 的旧版兼容性。不过,除了 ZX_RIGHT_GET_PROPERTYZX_RIGHT_SET_PROPERTY 之外,获取和设置 ZX_PROP_VMO_CONTENT_SIZE 将分别需要与 zx_vmo_get_stream_sizezx_vmo_set_stream_size 相同的权限(即 ZX_RIGHT_WRITE(用于设置 ZX_PROP_VMO_CONTENT_SIZE)。

实现

本部分介绍了每个 Zircon 系统调用的详细更改。

zx_pager_create_vmo

与现在一样,VMO 的大小会初始化为向上舍入到最接近的页面边界的大小。VMO 的流大小初始化为不进行任何舍入的大小

如果客户端提供 ZX_VMO_UNBOUNDED 选项,此操作会创建一个大小初始化为最大可能值的 VMO。

如果客户端同时提供 ZX_VMO_UNBOUNDEDZX_VMO_RESIZABLE 选项,此操作会返回 ZX_ERR_INVALID_ARGS

zx_pager_query_vmo_stats

无任何更改。不过,如果 VMO 包含数据流以外的修改,此操作可能会返回令人意外的结果。在未来的 RFC 中,我们预计会向 zx_pager_query_dirty_ranges 添加一个选项,以将查询限制为数据流。

zx_stream_create

如果 vmo 实参是指使用 zx_vmo_create_contiguousvmo_create_physical 创建的 VMO 对象,则此操作会返回 ZX_ERR_WRONG_TYPE

zx_stream_writev

如果此操作尝试写入超出数据流末尾的位置,则此操作会增加数据流的大小,类似于 zx_vmo_set_stream_size 更改数据流大小的方式。如果无法增加数据流,例如因为新的数据流大小会超过 VMO 的大小,则调整大小操作会失败。zx_stream_writev 的文档精确定义了如何处理这种情况,但大致来说,如果 writev 操作能够将任何数据写入 VMO,则该操作会成功并进行部分写入,否则会传播错误。

这些语义与此操作之前的语义不同。在此 RFC 之前,如果所需的流大小超过 VMO 的大小zx_stream_writev 会尝试更改 VMO 的大小。在此 RFC 之后,VMO 的大小永远不会因流操作而发生变化。

zx_vmo_create

与现在一样,VMO 的大小会初始化为向上舍入到最接近的页面边界的大小。VMO 的流大小初始化为不进行任何舍入的大小

如果客户端提供 ZX_VMO_UNBOUNDED 选项,此操作会创建一个大小初始化为最大可能值的 VMO。

如果客户端同时提供 ZX_VMO_UNBOUNDEDZX_VMO_RESIZABLE 选项,此操作会返回 ZX_ERR_INVALID_ARGS

zx_vmo_create_child

此操作的更改特定于模式:

  • ZX_VMO_CHILD_SNAPSHOT - 如果 size 不是内存页大小的倍数,此操作会返回 ZX_ERR_INVALID_ARGS。之前,此操作支持非对齐的大小值,但这种行为可能很危险,因为子 VMO 实际上可以访问父 VMO 中的整数个页面。

    子 VMO 的大小和流大小均初始化为 size 实参。子媒体资源的视频流大小可以独立于父媒体资源进行更改。

  • ZX_VMO_CHILD_SNAPSHOT_AT_LEAST_ON_WRITE - 行为与 ZX_VMO_CHILD_SNAPSHOT 类似。

  • ZX_VMO_CHILD_SLICE - 行为与 ZX_VMO_CHILD_SNAPSHOT 类似。

  • ZX_VMO_CHILD_REFERENCE - 子 VMO 的流大小初始化为父 VMO 的流大小

zx_vmo_create_contiguous

如果 size 不是页面大小的倍数,此操作会返回 ZX_ERR_INVALID_ARGS

VMO 的大小初始化为 size

VMO 的流大小初始化为零,无法修改。

vmo_create_physical

如果 size 不是页面大小的倍数,此操作会返回 ZX_ERR_INVALID_ARGS

VMO 的大小初始化为 size

VMO 的流大小初始化为零,无法修改。

zx_vmo_get_size

无任何更改。

zx_vmo_set_size

如果 size 实参不是页面大小的倍数,此操作会返回 ZX_ERR_INVALID_ARGS

将 VMO 的大小覆盖为 size 实参,并清零(即舍弃)超出此大小的任何页面。如果流大小大于新的 VMO 大小,则截断流大小以匹配新的 VMO 大小。

此行为与此操作之前的行为不同,之前此操作可用于增加 VMO 的内容大小。如果 VMO 存在,要将流大小增加到超出当前大小,请先使用 zx_vmo_set_size 增加 VMO 的大小,然后使用 zx_vmo_set_stream_size 增加流大小。

zx_vmo_get_stream_size

返回 VMO 的流大小

zx_vmo_set_stream_size

如果 size 实参大于 VMO 的大小,此操作会返回 ZX_ERR_OUT_OF_RANGE

将 VMO 的流大小覆盖为 size 实参,并从 size 和之前的流大小中较小的值开始到 VMO 的末尾进行清零。这与之前设置内容大小的行为不同,之前仅会从新的大小到 VMO 的末尾进行清零。这种差异意味着,当流大小增加时,新显示的范围会被清零。

此行为变更可确保新显示的内容始终为零,即使超出流大小的 VMO 已通过其他方式(例如直接内存映射)修改过也是如此。

如果此操作缩小了数据流,则不再与映射了 ZX_VM_FAULT_BEYOND_STREAM_SIZE 选项的数据流重叠的任何页面都会被取消映射。

无论是增长还是缩小,此操作都可能导致写时复制和用户分页器支持的 VMO 提交与数据流重叠的最后一页,以在该页中存储零。

该句柄必须具有 ZX_RIGHT_WRITE

zx_vmo_op_range

无任何更改。

zx_vmo_read

无任何更改。

此操作与 VMO 的大小(而非数据流大小)进行交互,这意味着此操作可以读取数据流之外的数据。

zx_vmo_write

无任何更改。

此操作与 VMO 的大小(而非数据流大小)进行交互,这意味着此操作可以写入数据流之外的数据。

zx_vmar_map

如果客户端提供 ZX_VM_FAULT_BEYOND_STREAM_SIZE 选项,则对 VMO 中包含数据流的最后一页之后的映射进行的内存访问将出现故障。此选项需要 ZX_VM_ALLOW_FAULTS 选项。

性能

此 RFC 不会对性能产生任何影响。主要变更与 Zircon 报告 VMO 元数据的方式有关。我们已选择每项操作的语义,以避免在实现过程中出现性能问题。

工效学设计

VMO 的概念模型较为复杂,因为有些操作将 VMO 视为内存页的集合,而另一些操作则与存储在 VMO 中的数据流进行交互。面向页面的操作以页面为粒度进行操作,而面向流的操作以字节为粒度进行操作。之所以会出现这种复杂性,是因为商品 CPU 中的虚拟内存硬件是围绕内存页设计的,而客户端存储在内存中的数据是围绕字节设计的。

此 RFC 尝试通过更清晰地分离基于网页的操作和基于流的操作来消除这些概念的歧义。例如,zx_vmo_set_size 是基于内存页的操作,因此需要与内存页对齐的大小,而 zx_vmo_set_stream_size 是基于流的操作,因此支持与字节对齐的大小。

有些情况比较细微,例如 zx_vmo_readzx_vmo_write。合理的客户端可能会认为这些操作会与数据流进行交互,因为它们会读取和写入数据。不过,我们选择让这些操作反映通过映射内存上的 loadstore 操作获得的语义,这意味着它们会与 VMO 的面向页面的大小进行交互。

这种情况过于受限,尤其是考虑到迭代设计大量现有软件的系统时,路径依赖性非常强。在设计限制条件下,我们已尽最大努力寻找最符合人体工程学的解决方案。

向后兼容性

此更改会带来一些向后兼容性风险。例如,zx_vmo_create_childzx_vmo_create_contiguouszx_vmo_create_physical 现在需要页面对齐的大小,并且设置 ZX_PROP_VMO_CONTENT_SIZE 需要额外的权限,这两者都会破坏一些现有代码。我们认为可以将该代码迁移为符合这些新限制,但如果无法执行这些迁移,我们可能需要放宽这些限制。

此 RFC 中的其他更改不太可能导致向后兼容性问题。在大多数情况下,现有语义会保留,只是以不同的术语进行说明。

安全注意事项

此 RFC 中的主要安全风险在于,数据可能存在于 VMO 中,而不仅仅是 VMO 中的数据流。这种行为可能会让许多开发者感到意外,而意外情况往往会带来安全风险。不过,这些操作的典型用法会导致相应数据始终为零,并且其他广泛使用的操作系统也存在此风险。

隐私注意事项

从理论上讲,如果超出数据流范围存储在 VMO 中的数据意外泄露,可能会造成隐私问题。不过,在正常运行中,VMO 数据流之外的数据将为零。

测试

我们将添加适当的 Zircon 核心测试,以验证对系统调用语义的所有更改。

文档

在实现这些更改后,系统调用文档将更新以反映新的语义。

缺点、替代方案和未知因素

在项目过程中,我们尝试了各种替代方案,但效果都不理想。理想情况下,VMO 应具有一致的心理模型,该模型基于字节级或页面级大小构建。不过,字节粒度有很强的应用场景,因为业界已将 8 位字节标准化为数据的大小粒度,并且在以页面粒度处理内存时存在很强的基于硬件的限制。因此,这两种极端情况都不是可行的设计点。

实际上,此 RFC 的主要替代方案是不执行任何操作,并保留当前 VMO 的双尺寸模型。不过,我们认为此 RFC 中的小幅更改是对当前模型的改进。我们曾考虑对模型进行更大幅度的更改,但在每种情况下,我们都发现了用例或设计限制,最终促使设计回归到这种更适中的方法。

在先技术和参考资料

这一领域有大量的现有技术。主要相关现有技术是文件在其他热门操作系统中与内存映射的交互方式。在我们研究的所有案例中,文件都具有字节粒度,而内存映射具有页面粒度。我们特别研究了文件最后一页(例如,超出文件内容末尾)的内存映射如何运行。ZX_VM_FAULT_BEYOND_STREAM_SIZE 标志旨在让我们复制在 Windows、macOS 和 Linux 中观察到的语义。不过,我们并未选择这些语义作为默认语义,因为 zx_vmar_map 默认情况下不允许出现故障。我们预计,大多数构建内存映射的跨平台代码(例如 mmap)将提供此标志,以使语义在各个操作系统之间保持一致。