RFC-0226:Zircon 分页器回写

RFC-0226:Zircon 分页器回写
状态已接受
区域
  • 内核
说明

内核支持跟踪和回写对页面器支持的 VMO 的修改

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2023-04-13
审核日期(年-月-日)2023-09-19

摘要

本文档介绍了 Zircon 内核对有分页支持的内存的支持。这种内存可修改,然后写回(同步)到分页源(例如存储磁盘)。

设计初衷

Zircon 支持创建由用户空间分页器服务(通常由文件系统托管)支持的 VMO(虚拟内存对象)。单个文件在内存中表示为 VMO。当访问 VMO 的页面时,系统会按需将其引入错误状态,并由用户分页器从磁盘读取页面内容。

Zircon 分页器 API 最初仅支持只读文件;没有任何机制可将内存中已被污染的 VMO 页面写回磁盘。这种设计足以托管 blobfs 等不可变文件系统,该文件系统会提供 Fuchsia 上的所有可执行文件和其他只读文件。但是,对于通用文件系统,需要写回支持,因为客户端可以修改文件内容,并且需要将其同步回磁盘。

如果不支持回写,minfsfxfs 等可变文件系统将无法利用按需分页。作为一种权宜解决方法,可变文件系统必须使用匿名(非页面器支持)VMO 将文件缓存在内存中,并自行管理这些 VMO 的内容。即使其中的某些页面很少或从不访问,这些 VMO 可能也需要全部保留在内存中。将这些匿名 VMO 切换为由分页器支持的 VMO(其中的页面可以根据需要进行错误处理和驱逐),可让可变文件系统更好地利用内存。回写支持还允许可变文件系统的客户端直接对 VMO 执行读写操作,而不是依赖于通道进行数据传输,后者受通道限制,速度可能非常慢。

在本文档的其余部分中,术语“用户分页器”和“文件系统”可互换使用,具体取决于上下文。

利益相关方

教员

  • cpu@google.com

Reviewers:

  • adanis@google.com、custer@google.com

咨询了

  • brettw@google.com、cdrllrd@google.com、godtamit@google.com、 maniscalco@google.com、travisg@google.com

社交

此 RFC 已通过本地存储团队的设计审核。

要求

所提议的设计旨在实现以下目标:

  1. 增强了 Zircon,以支持对由分页器支持的 VMO 进行回写,从而允许构建高性能可变文件系统(使用 Zircon 流)。
  2. 支持通过虚拟机映射执行文件读写操作(例如 mmap 文件)。
  3. 提供由用户分页器发起的尽力刷新脏页功能,以降低因意外关闭而导致数据丢失的风险。
  4. 在未来的迭代中,允许内核(通过用户分页器)驱逐脏页以响应系统内存压力。

以及一些非目标:

  1. 除了用户分页器发起的刷新之外,内核对脏页清理速率不做任何保证。不过,未来的演变可能会增加限制未处理脏数据量以及让内核根据该量发起回写请求的功能。
  2. 防止因违反内核/分页器协议而导致数据丢失不是我们的目标。如果用户分页器在关闭其对 VMO 的句柄(或终止)之前未能查询脏页并将其写回,则可能会发生数据丢失。

设计

概览

拟议的设计旨在支持以下两种直接用例:

  • 文件系统客户端可以通过流访问文件,流是封装文件 VMO 的内核对象。这可以粗略地视为与 zx_vmo_read()zx_vmo_write() 类似,因为流系统调用会在内部封装 VMO 读/写内核例程。
  • 文件系统客户端还可以 mmap 文件,这大致相当于将文件 VMO 映射 (使用 zx_vmar_map) 到客户端进程的地址空间。

为简单起见,本文档的其余部分将介绍如何通过系统调用 (zx_vmo_read/write) 或虚拟机映射 (zx_vmar_map) 与文件 VMO 直接交互。

以下是几个示例,展示了涉及回写的互动可能是什么样的。

示例 1

  1. 文件系统客户端针对给定范围对文件 VMO 执行 zx_vmo_read()
  2. 由于 VMO 由分页器支持,因此内核会为关联的用户分页器生成读取请求。
  3. 文件系统托管的用户分页器会执行此请求。它会为页面提供从磁盘读取的内容。
  4. 文件系统客户端针对同一范围对 VMO 执行 zx_vmo_write()。VMO 的网页之前已在第 3 步中填充,因此只需写入即可。所做的修改目前仅存在于内存中,但需要在某个时间点反映回磁盘。
  5. 用户分页器会向内核查询 VMO 中已被污染 / 修改的范围。这可以作为文件系统执行的定期后台刷新的一部分来完成,也可以作为对文件系统客户端请求的刷新做出响应来完成。
  6. 用户分页器会将查询到的脏页范围写回磁盘。此时,修改后的文件内容已成功持久保存到磁盘。

示例 2

  1. 文件系统客户端使用 zx_vmar_map() 映射文件 VMO。映射从地址 addr 开始。客户端读取从 addr 开始的范围。
  2. 与示例 1 相同。
  3. 与示例 1 相同。
  4. 文件系统客户端现在会写入从 addr 开始的同一范围。底层页面已填充,因此内容会在内存中修改。这些修改需要在某个时间点反映回磁盘。
  5. 与示例 1 相同。
  6. 与示例 1 相同。

以下示例会先执行 VMO 读取,然后再执行写入。请注意,这样做只是为了将按用户分页器填充网页的操作拆分为单独的步骤,以便于理解。客户端可以直接写入尚未在内存中的文件偏移量;写入操作只会阻塞,直到用户分页器先提供页面。

上述两个示例都假定文件系统在写入时遵循覆盖模型,即可以直接写入已填充(已提交)的页面,而无需先请求额外的空间。修改后的内容会写回磁盘上的同一位置,因此无需为修改分配额外的空间。不过,fxfsminfs 等文件系统使用的是“写时复制”(CoW) 模型,其中每次修改都需要在磁盘上分配新的空间。因此,我们还需要一种机制来为写入已提交的页面预留空间;第 4 步已修改为在等待该预留操作完成后才能继续写入。

为了执行回写,Zircon 分页器 API 已扩展为支持以下操作:

  • 内核会阻止对用户分页器已指明应遵循写时复制方案的 VMO 进行写入,并在用户分页器确认写入后继续操作。
  • 内核会跟踪 VMO 中的脏页,并具有向用户分页器显示此类信息的机制。
  • 用户分页器会向内核指明它何时在同步 VMO 中的脏页范围以及何时完成同步,以便内核可以相应地更新脏页跟踪状态。
  • 内核还会向用户分页器显示有关 VMO 大小调整的信息。
  • 用户分页器可以查询内核代表其跟踪的相关信息,例如 VMO 的上次修改时间。

创建 Pager 和 VMO

分页器创建系统调用保持不变,即使用 zx_pager_create() 创建分页器,并将 options 设置为 0。

由分页器支持的 VMO 使用 zx_pager_create_vmo() 创建,并与分页器端口以及将在该 VMO 的页面请求数据包中使用的密钥相关联。zx_pager_create_vmo() 系统调用还支持新的 options 标志 ZX_VMO_TRAP_DIRTY。这表示内核应捕获对 VMO 的任何写入,并先从用户分页器请求写入确认。此标志适用于以写时复制模式运行的文件。稍后会详细介绍此标志。

// Create a VMO (returned via |out|) backed by |pager|. Pager requests will be
// queued on |port| and will contain the provided |key| as an identifier.
// |size| will be rounded up to the page boundary.
//
// |options| must be 0 or a combination of the following flags:
// ZX_VMO_RESIZABLE - if the VMO can be resized.
// ZX_VMO_TRAP_DIRTY - if writes to clean pages in the VMO should be trapped by the
// kernel and forwarded to the pager service for acknowledgement before proceeding
// with the write.
zx_status_t zx_pager_create_vmo(zx_handle_t pager,
                                uint32_t options,
                                zx_handle_t port,
                                uint64_t key,
                                uint64_t size,
                                zx_handle_t* out);

默认情况下,所有由分页器支持的 VMO 都被视为可变的;这也适用于在不增加费用的情况下实现只读文件系统。修改页面的代码路径可能不会针对只读 VMO 进行调用。不过,如果 VMO 被修改(可能是意外滥用),但用户分页器从未查询其中的脏页并尝试将其写回,则修改后的内容将仅保留在内存中。今后,当内核生成回写请求时,用户分页器可以将此类 VMO 的回写请求视为错误,也可以直接忽略它们。

提供 VMO 页面

在收到分页器读取请求后,用户分页器会按需使用 zx_pager_supply_pages() 填充由分页器支持的 VMO 中的页面。此系统调用已存在,可与只读 VMO 搭配使用。

回写的页面状态

由分页器支持的 VMO 中的页面可以有三种状态:DirtyCleanAwaitingClean。这些状态在 vm_page_t 中编码。

这三种状态之间的转换遵循以下步骤:

  1. 新提供 zx_pager_supply_pages() 的页面最初为 Clean
  2. 写入页面后,它会转换为 Dirty。如果 VMO 是使用 ZX_VMO_TRAP_DIRTY 创建的,则内核会先在收到来自用户分页器的 DIRTY 分页器请求确认后阻塞。稍后会详细介绍这项互动。
  3. 用户分页器稍后会通过系统调用向内核查询 VMO 中的脏页列表。
  4. 对于内核返回的每个脏页,用户分页器都会调用一个系统调用,以向内核发出信号,表明它开始回写页面,从而将其状态更改为 AwaitingClean
  5. 如果写入页面超出此点,其状态会切换回 Dirty
  6. 用户分页器在写回页面后会发出另一个系统调用。如果此时页面状态为 AwaitingClean,则会转换为 Clean
  7. 如果用户分页器在回写时遇到错误,页面会保持在 AwaitingClean 状态。对脏页的未来查询会同时返回 AwaitingCleanDirty 页,以便用户分页器可以尝试再次写回页面。

下图状态图展示了这些状态之间的转换。

脏状态转换图

AwaitingClean 需要作为单独的状态进行跟踪,原因有以下几点:

  • 尽管处于 CleanAwaitingClean 状态的页面在写入时都会转换为 Dirty,但用户分页器正在写回的页面需要与 Clean 页面区别对待。在内存压力下,Clean 页面可以回收,但正在回写的页面需要受到保护,以免被回收。

  • 内核需要知道已写回哪个版本的页面,以便在用户分页器完成时正确将其转换为 Clean。这一点很重要,因为它有助于区分在刷新之前收到的页面写入(这些写入将安全地写入磁盘)与在刷新之后收到的页面写入(这些写入需要稍后写回)。

  • 我们可以在回写开始时避免从用户分页器发出系统调用,并且内核在将页面作为脏页查询的一部分返回给用户分页器时,只需将页面标记为 AwaitingClean 即可。不过,在查询后,用户分页器可能还需要一段时间才能开始刷出页面,这会让页面再次变脏的时间延长。缩小回写边界窗口可提高用户分页器成功将页面移至 Clean 状态的可能性。

为了更新脏状态,内核会跟踪何时通过 VMO 和流式写入系统调用以及通过虚拟机映射写入页面。请注意,这适用于通过虚拟机映射发生的任何写入(无论是用户还是内核执行的),即也适用于内核使用 user_copy 作为 zx_channel_read() 等系统调用的一部分执行的写入。

由于范围已指定,因此在 zx_vmo_write() 等系统调用期间推断脏页非常简单。访问 VMO 的另一种方式是通过进程地址空间中的虚拟机映射。为了跟踪由分页器支持的 VMO 中的脏状态,可写映射在开始时会从相应页表条目中移除写入权限。因此,写入会生成保护故障,可通过恢复写入权限并将页面状态标记为 Dirty 来解决此问题。

此处提及的 Dirty 状态是指 vm_page_t 跟踪的状态,即软件跟踪的脏状态。x86 上的硬件页表支持脏位跟踪,但我们选择不使用它来派生初始实现的页面脏状态。对于不支持页表中脏位标志的旧版 arm64 平台,我们无论如何都需要在软件中跟踪脏位标志。因此,为了实现一致性和简单性,我们选择先不使用硬件脏位来推断页面的脏 / 清状态。依赖于硬件页表位还会导致页表回收复杂化,因此在未来我们依赖硬件位时,需要考虑这一点。

值得注意的是,只有直接由分页器支持的 VMO 才符合脏数据跟踪条件。换句话说,由分页器支持的 VMO 的 CoW 克隆不会选择启用脏数据跟踪,也不会看到任何回写请求。在克隆中编写的网页会从父级网页的副本分叉,并且克隆会直接将其作为单独的网页拥有。

为待处理的写入预留空间

对使用 ZX_VMO_TRAP_DIRTY 创建选项标记为捕获脏数据转换的 VMO 进行写入需要文件系统的确认。该解决方案分为两个部分,v1 从简单入手,更侧重于正确性,v2 则在 v1 的基础上改进性能。v1 提案主要遵循同步模型,文件系统会为新写入预留空间。在 v2 中,我们将添加另一层,用于表示内核中的脏预留配额以及它们如何应用于 VMO,以便内核能够自行跟踪预留。这有助于减少内核和文件系统之间的大部分来回通信,从而提升性能。

ZX_VMO_TRAP_DIRTY v1

ZX_VMO_TRAP_DIRTY VMO 创建标志表示内核应在 VMO 中捕获任何 Clean->Dirty 页面转换(或 AwaitingClean->Dirty 转换)。当写入尚未脏的页面时,内核会生成 ZX_PAGER_VMO_DIRTY 分页器请求。对于通过虚拟机映射进行的写入,请求会跨包含故障地址的单个页面。对于数据流/VMO 写入,内核会针对需要写入的范围内每个连续的非脏页运行发送一个请求。

范围为 [start, end) 的脏请求如下所示。

zx_packet_page_request_t request {
    .command = ZX_PAGER_VMO_DIRTY,
    .flags = 0,
    // |offset| and |length| will be page-aligned.
    .offset = start,
    .length = end - start,
};

ZX_VMO_TRAP_DIRTY 创建标志适用于以 CoW 模式写入的文件,以及以覆盖模式写入的稀疏文件。如果未指定此标志,系统会将页面标记为 Dirty,并且在写入时不会涉及用户分页器;此标志适用于以覆盖模式写入的非稀疏文件。

用户分页器使用 zx_pager_op_range() 确认 ZX_PAGER_VMO_DIRTY 请求:

  • ZX_PAGER_OP_DIRTY 会将尚未处于 Dirty 状态的页面状态设置为 Dirty,然后内核会继续执行被阻塞的写入操作。
  • ZX_PAGER_OP_FAIL 不会更改页面的当前状态,并且会使发起写入的 zx_vmo_write() 调用失败,为虚拟机映射生成严重的页面故障异常,对于 zx_stream_write(),则会成功返回部分写入。
// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |offset| and |length| specify the range, i.e. [|offset|, |offset| + |length|).
//
// |op| can be:
//
// ZX_PAGER_OP_DIRTY - The userspace pager wants to transition pages in the range
// [offset, offset + length) from clean to dirty. This will unblock any writes that
// were waiting on ZX_PAGER_VMO_DIRTY page requests for the specified range.
// |data| must be 0.
//
// ZX_PAGER_OP_FAIL - The userspace pager failed to fulfill page requests for
// |pager_vmo| in the range [offset, offset + length) with command
// ZX_PAGER_VMO_READ or ZX_PAGER_VMO_DIRTY.
//
// |data| contains the error encountered, a zx_status_t error code sign-extended
// to a |uint64_t| value - permitted values are ZX_ERR_IO, ZX_ERR_IO_DATA_INTEGRITY,
// ZX_ERR_BAD_STATE and ZX_ERR_NO_SPACE.
zx_status_t zx_pager_op_range(zx_handle_t pager,
                              uint32_t op,
                              zx_handle_t pager_vmo,
                              uint64_t offset,
                              uint64_t length,
                              uint64_t data);

当客户端写入页面时,此方法可能会导致显著的性能开销,具体取决于文件系统刷新脏数据和标记页面 Clean 的频率。为了避免此开销,文件系统可能希望尽可能推迟刷新脏数据,但这不是一个好主意,因为脏页无法被驱逐,会导致内存压力增加,并且刷新间隔时间越长,数据丢失的可能性也越大。v2 方案尝试降低面向客户端的写入带来的部分性能开销。

ZX_VMO_TRAP_DIRTY v2

系统将添加一个新的系统调用 zx_pager_set_dirty_pages_limit(),用于指定内核允许累积的脏页数量。预计文件系统会预先为这么多脏页预留空间。这是每个分页器的限制,默认设置为零。您可以使用 zx_pager_set_dirty_pages_limit() 将此限制设置为非零值(如果需要,可以多次设置)。v1 设计在本质上将此限制设置为零。

zx_status_t zx_pager_set_dirty_pages_limit(zx_handle_t pager_handle,
                                           uint64_t num_dirty_pages);

内核将跟踪每个分页器的脏页数量(稍后会详细介绍符合跟踪条件的页面类型),在转换为 Dirty 时将计数递增,在转换为 Clean 时将计数递减。内核仍会像 v1 中一样捕获每个脏页转换,但只会递增未处理的脏页数量(如果可以这样做且不会超出分配的脏页限制)。如果新计数不超过上限,内核将继续进行写入,而不会涉及用户分页器。这应该是正常的操作模式,因此可以避免每次页面被污染时都需要往返用户分页器的开销。

采用这种方法时,用户分页器需要向内核传达以下两项信息:

  1. 整个分页器的脏页上限
  2. 在被污染时会计入该限制的网页

对于 2),我们再次依赖于 ZX_VMO_TRAP_DIRTY VMO 创建标志。此标志现在会触发生成新类型的页面浏览器请求:ZX_VMO_DIRTY_MODE。现在,当内核陷阱写入时,它会咨询文件系统,以确定是否应选择将这些页面计入脏页限制。用户分页器会使用 zx_pager_op_range 响应,其中包含两种新操作类型之一。

  • ZX_PAGER_OP_DIRTY_MODE_POOLED 会告知内核,该范围内的页面将计入每个页面脏页数上限。这适用于在 CoW 模式下运行的文件,以及在覆盖模式下文件的稀疏区域。

  • ZX_PAGER_OP_DIRTY_MODE_UNPOOLED 会告知内核,该范围内的页面不会计入脏页限制。这适用于以覆盖模式运行的稀疏文件的非稀疏区域。

指示 ZX_PAGER_OP_DIRTY_MODE_POOLED 的页面会转换为 Dirty,并且未处理的页面脏读计数会递增(前提是该计数不超过页面脏读限制)。不过,如果使页面脏会超出分页器脏页限制,则内核会开始生成 ZX_PAGER_VMO_DIRTY 数据包,即 v1 中所述的默认模式。可以提供一个可选标志,以便在回写页面(转换为 Clean)时设置脏模式,这将节省捕获未来写入以生成 ZX_VMO_DIRTY_MODE 页面游标请求的费用。

这种设计提供了一种灵活的模型,文件系统可以在其 VMOs 上混合使用不同类型的写入模式。以 COW 模式写入的文件的 VMO 将使用 ZX_VMO_TRAP_DIRTY 创建,并且其页面可以使用池化模式。同样,您可以使用 ZX_VMO_TRAP_DIRTY 标志创建覆盖模式下的稀疏文件,并分别针对稀疏区域和非稀疏区域使用“池化”和“非池化”模式。始终使用“覆盖”模式的文件可以完全省略 ZX_VMO_TRAP_DIRTY 标志,并且在写入时永远无需支付分页器请求的费用。

当用户分页器耗尽脏页配额后,开始接收 ZX_PAGER_VMO_DIRTY 请求后,预计会开始清理页面,以便为新的脏页创建空间。完成后,它会使用之前或新的上限通过 zx_pager_set_dirty_pages_limit() 发出信号。此调用之后,内核将在日后的写入中恢复检查累积脏数据计数与脏数据限制的对比情况,并且仅在再次达到脏数据限制时生成 ZX_PAGER_VMO_DIRTY 请求。

ZX_VMO_TRAP_DIRTY v1 和 v2 之间的差异

v1 和 v2 之间的主要区别在于负责跟踪预订数量的实体。在 v1 中,文件系统负责跟踪预留,内核会告知文件系统何时以及要将预留数量增加多少。由于负责拦截预留的潜在更改的实体(内核)与执行实际记账的实体(文件系统)不同,因此这两者之间需要紧密耦合。在 v2 中,我们尝试通过让内核自行跟踪预留计数来稍微放宽此限制。因此,只有在以下情况下才需要与文件系统通信:1) 需要设置 VMO 范围以选择启用(或停用)内核预留跟踪;2) 内核耗尽预留配额,并且文件系统需要进行干预。

我们预计 2) 是此处的极端情况,因为文件系统会定期将脏页刷新到磁盘。大部分通信预计是由于 1)内核可以多次请求同一范围的信息(例如,对于跨重叠范围的写入),同样,文件系统也可以多次向内核提供同一范围的冗余信息。在页面上设置脏模式实际上不会对脏页数上限造成任何影响,因为只有在实际写入页面时,脏页数才会递增。因此,文件系统还可以推测性地为页面设置脏模式,以减少未来分页器请求的性能开销(由于可能会发生页面驱逐,因此存在一些注意事项)。

发现脏范围

用户分页器需要一种机制来了解 VMO 中的脏页,以便将其写回。这里有两种不同的模型需要考虑:当用户分页器从内核查询脏页信息时,使用拉取模型;当内核通过发送用户分页器回写请求来指示脏页时,使用推送模型。初始设计从更简单的拉取模型开始,并引入了脏范围查询系统调用,该调用可能如下所示:

// |pager| is the pager handle.
// |pager_vmo| is the vmo handle.
// |offset| and |length| specify the VMO range to query dirty pages within.
// Must be page-aligned.
//
// |buffer| points to an array of type |zx_vmo_dirty_range_t| defined as follows.
// typedef struct zx_vmo_dirty_range {
//   // Represents the range [offset, offset + length).
//   uint64_t offset;
//   uint64_t length;
//   // Any options applicable to the range.
//   // ZX_VMO_DIRTY_RANGE_IS_ZERO indicates that the range contains all zeros.
//   uint64_t options;
// } zx_vmo_dirty_range_t;
//
// |buffer_size| is the size of |buffer|.
//
// |actual| is an optional pointer to return the number of dirty ranges that were
// written to |buffer|.
//
// |avail| is an optional pointer to return the number of dirty ranges that are
// available to read. If |buffer| is insufficiently large, |avail| will be larger
// than |actual|.
//
// Upon success, |actual| will contain the number of dirty ranges that were copied
// out to |buffer|. The number of dirty ranges that are copied out to |buffer| is
// constrained by |buffer_size|, i.e. it is possible for there to exist more dirty
// ranges in [offset, offset + length) that could not be accommodated in |buffer|.
// The caller can assume than any range that had been made dirty prior to
// making the call will either be contained in |buffer|, or will have a start
// offset strictly greater than the last range in |buffer|. Therefore, the caller
// can advance |offset| and make another query to discover further dirty ranges,
// until |avail| is zero.
//
zx_status_t zx_pager_query_dirty_ranges(zx_handle_t pager,
                                        zx_handle_t pager_vmo,
                                        uint64_t offset,
                                        uint64_t length,
                                        void* buffer,
                                        size_t buffer_size,
                                        size_t* actual,
                                        size_t* avail);

用户分页器应多次调用此查询,并推进其正在查询的偏移量,直到处理完所有脏页。

在拉取模型中,清理页面的速率完全取决于文件系统选择查询脏范围和尝试回写的速率。不过,在某些情况下,内核本身可能需要发起写回页面的请求(例如在内存压力下),以便清理脏页并随后释放它们。在这种情况下,内核可能会按 LRU 顺序发送脏页写回请求。这是为了向用户分页器提供提示,以便其加快页面刷新速率(例如,如果它以延迟方式处理请求)。

针对 VMO 中脏范围 [start, end) 的回写请求可能如下所示。

zx_packet_page_request_t request {
    .command = ZX_PAGER_VMO_WRITEBACK,
    .flags = ZX_PAGER_MEMORY_PRESSURE,
    // |offset| and |length| will be page-aligned.
    .offset = start,
    .length = end - start,
};

回写脏范围

zx_pager_op_range() 系统调用已扩展为支持另外两个操作 ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END,分别用于指示用户分页器何时开始刷新页面以及何时完成刷新。

  • ZX_PAGER_OP_WRITEBACK_BEGIN 会将指定范围内的所有 Dirty 页面的状态更改为 AwaitingClean。对于已处于 AwaitingCleanClean 状态的任何网页,系统都会忽略此参数,并保持这些状态不变。
  • ZX_PAGER_OP_WRITEBACK_END 会将指定范围内的所有 AwaitingClean 页面的状态更改为 Clean。系统会忽略已为 CleanDirty 的任何网页,并保持其状态不变。

如果在执行刷新(即在 ZX_PAGER_OP_WRITEBACK_BEGIN 之后但在 ZX_PAGER_OP_WRITEBACK_END 之前)期间遇到任何错误,用户分页器无需执行任何其他操作。假设没有其他写入操作,这些页面会在内核中保持 AwaitingClean 状态。当再次向内核查询脏页时,内核将包含 AwaitingClean 页面以及 Dirty 页面,然后用户分页器可以再次尝试对这些失败的页面进行回写。

// Supported |op| values are:
// ZX_PAGER_OP_WRITEBACK_BEGIN indicates that the user pager is about to
// begin writing back the specified range and the pages are marked |AwaitingClean|.
// ZX_PAGER_OP_WRITEBACK_END indicates that that user pager is done writing
// back the specified range and the pages are marked |Clean|.
//
// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |offset| and |length| specify the range to apply the |op| to, i.e. [|offset|,
// |offset| + |length|).
// For ZX_PAGER_OP_WRITEBACK_*, |data| is unused and should be 0.
zx_status_t zx_pager_op_range(zx_handle_t pager,
                              uint32_t op,
                              zx_handle_t pager_vmo,
                              uint64_t offset,
                              uint64_t length,
                              uint64_t data);

对于 ZX_PAGER_OP_WRITEBACK_BEGINdata 可以选择设置为 ZX_VMO_DIRTY_RANGE_IS_ZERO,以指示调用方希望将指定范围写回为零。当调用方处理 zx_pager_query_dirty_ranges() 返回的范围(其 options 设置为 ZX_VMO_DIRTY_RANGE_IS_ZERO)时,应使用此方法。它通过错误地假定该范围在查询后但在开始回写之前创建的所有非零内容仍为零并将其标记为干净(因此可驱逐),从而确保这些内容不会丢失。

调整 VMO 的大小

有分页器支持的 VMO 与匿名(无分页器支持)VMO 在处理 VMO 中内容缺失的方式上有所不同。匿名 VMO 的隐式初始内容为零,因此未提交的页面也隐含零。这不适用于由分页器支持的 VMO,其中未提交的页面并不意味着零;它们只是表示分页器尚未为这些页面提供内容。不过,如果调整为更大的大小,分页器无法在新扩展的范围内提供页面,原因很简单,因为相应内容尚未存在于后备来源(例如存储磁盘)上,因此没有任何内容可分页。内核可以将此新扩展范围内的页面作为零提供,而无需咨询用户分页器。

系统通过跟踪跨新调整大小范围的零间隔来处理调整大小,内核会为此隐式提供零页。用户分页器尚不了解此零间隔,因此当用户分页器查询脏数据范围时,此范围会被报告为脏数据。此外,对于此范围,zx_vmo_dirty_range_t 中的 options 字段设置为 ZX_VMO_DIRTY_RANGE_IS_ZERO,以指示其全为零。

如果 VMO 是使用 ZX_VMO_TRAP_DIRTY 标志创建的,并且页面写入到此新扩展的范围,则内核会在提交这些页面之前为它们生成 ZX_PAGER_VMO_DIRTY 分页器请求。这是因为文件系统可能需要为实际(非零)页面预留空间。此模型假定文件系统可以将零高效地表示为磁盘上的稀疏区域,因此只有在将页面提交到新扩展的范围时才会咨询文件系统。

将 VMO 与分页器分离

zx_pager_detach_vmo() 会将 ZX_PAGER_COMPLETE 数据包加入队列,这表示用户分页器日后不应再收到针对该 VMO 的进一步分页器请求。这也表明,用户分页器应查询并回写所有未处理的脏页。请注意,在脏页写回之前,分离操作不会阻塞;它只会通知用户分页器可能需要刷新。

分离后,原本需要生成分页器请求的 zx_vmo_read() / zx_vmo_write() 会失败并返回 ZX_ERR_BAD_STATE。通过类似情况下需要分页器请求的映射进行读写会生成严重的页面故障异常。内核可以随意从 VMO 中舍弃干净页面。不过,内核会保留脏页,直到用户分页器清理这些页面。也就是说,即使 VMO 已分离,ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END 仍受支持。所有其他操作均为 zx_pager_op_range(),分离的 vmo 上的 zx_pager_supply_pages() 失败并显示 ZX_ERR_BAD_STATE

如果页面整理器被销毁,并且关联的 VMO 中存在脏页,则无论是否存在任何待处理的回写请求,内核都可以在该时刻移除这些页面。换句话说,只要有页面整理器可用于清理脏页,脏页就一定会保留在内存中。

查询分页器 VMO 统计信息

内核还会跟踪 VMO 是否已修改,用户分页器可以查询此信息。此接口供用户分页器跟踪 mtime

// |pager| is the pager handle.
// |pager_vmo| is the VMO handle.
// |options| can be ZX_PAGER_RESET_VMO_STATS to reset the queried stats.
// |buffer| points to a struct of type |zx_pager_vmo_stats_t|.
// |buffer_size| is the size of the buffer and should be large enough to
// accommodate |zx_pager_vmo_stats_t|.
//
// typedef struct zx_pager_vmo_stats {
//   uint32_t modified;
// } zx_pager_vmo_stats_t;
zx_status_t zx_pager_query_vmo_stats(zx_handle_t pager,
                                     zx_handle_t pager_vmo,
                                     uint32_t options,
                                     void* buffer,
                                     size_t buffer_size);

如果修改了 VMO,则返回的 zx_pager_vmo_stats_tmodified 字段设置为 ZX_PAGER_VMO_STATS_MODIFIED;否则设置为 0。zx_pager_vmo_stats_t 结构体未来可以扩展为包含更多字段,以便用户分页器查询。

在修改 VMO 的系统调用(例如 zx_vmo_write()zx_vmo_set_size())以及首次通过映射传入写入页面故障时,系统会更新 modified 状态。系统已跟踪页面上的首次写入故障,以便正确管理 CleanDirty 的转换,因此 modified 状态会随之更新。不过,系统不会跟踪通过脏页映射传入的未来写入;如果这样做,写入映射的 VMO 的速度会明显变慢。因此,对于映射的 VMO,modified 状态可能并不完全准确。

如果用户分页器还希望重置查询的统计信息,options 可以是 ZX_PAGER_RESET_VMO_STATSoptions 值为 0 不会重置任何状态,并且会执行纯查询。请注意,如果指定了 ZX_PAGER_RESET_VMO_STATS 选项,此调用可能会消耗可查询状态,从而影响未来的 zx_pager_query_vmo_stats() 调用。例如,如果 zx_vmo_write() 后跟两个连续的 zx_pager_query_vmo_stats() 调用(使用 ZX_PAGER_RESET_VMO_STATS 选项),则只有第一个调用会看到 modified 的设置。由于第一个 zx_pager_query_vmo_stats() 之后没有进行进一步修改,因此第二个 zx_pager_query_vmo_stats() 将返回 modified 为 0。

实现

分页器回写已经开发了一段时间,@next vDSO 中提供了所有新的 API 部分。fxfs 采用了回写 API 来支持流式读写和 mmap

性能

借助分页器回写,fxfs 能够从通过通道执行 I/O 切换为使用数据流,这在各种基准测试中使性能提升了约 40-60 倍。

安全注意事项

无。

隐私注意事项

无。

测试

我们编写了内核核心测试和压力测试来运行分页器系统调用。还有存储测试和性能基准。

文档

更新了内核系统调用文档。

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

对写回请求进行速率限制

在未来的迭代中,当内核生成回写请求时(在内存压力下或以稳定的后台速率),我们需要某种政策来控制在分页器端口上排队的回写请求数量。一种方法是让内核跟踪正在处理的未处理请求的数量,并尝试将其保持在一定限制内。

另一种方法是,用户分页器配置可调整项,这些可调整项直接或间接地决定了回写请求生成率。例如,用户分页器可以指定页面在加入写回队列之前可以保持脏状态的建议时长,或者用户分页器可以支持的写入典型数据传输速率。某些文件系统可能需要的后台写回速率远高于全局系统默认值。用户分页器还可以指定其处理请求的精细程度(系统页面大小的倍数)。然后,内核在计算范围时可以将此考虑在内,并且总体上可以生成更少的请求。

跟踪和查询网页年龄信息

对于初始实现,内核会在页面队列中跟踪脏页,该队列按页面首次被标记为脏的时间排序。此队列可用于日后生成回写请求,并且页面清理完毕后,可以从脏队列移至由页面整理器支持的(干净)队列,后者目前用于跟踪和老化只读页面。我们可能还希望更精细地跟踪脏页的使用年龄;将脏页和干净页合并到一个公共池中可能很有意义,以便利用与全局工作集相关的使用年龄和已访问位跟踪功能。我们可能还希望通过新 API 将此年龄信息公开给用户分页器,以便在处理回写请求时将其考虑在内。

在回写期间阻止进一步写入

这里提出的设计不会阻止在回写过程中(即 ZX_PAGER_OP_WRITEBACK_BEGINZX_PAGER_OP_WRITEBACK_END 之间)收到的新写入。相反,系统只会再次将写入的页面标记为脏页。某些文件系统可能希望在页面处于 AwaitingClean 状态时阻止对页面的写入。我们可能会考虑将来添加 ZX_PAGER_OP_WRITEBACK_BEING_SYNC,以在回写期间阻止写入。请注意,ZX_VMO_TRAP_DIRTY v1 确实提供了一种使用 ZX_PAGER_VMO_DIRTY 页面器请求解决此问题的方法,文件系统可以在刷新期间暂停处理这些请求。

在先技术和参考文档