RFC-0226:Zircon 分页器回写

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

内核支持,可跟踪和回写由分页器支持的 VMO 的修改

问题
  • 63989
Gerrit 更改
  • 827880
作者
审核人
提交日期(年-月-日)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

审核者

  • adanis@google.com、csuter@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 操作,这会大致转换为(使用 zx_vmar_map)将文件 VMO 映射到客户端进程的地址空间。

为简单起见,文档的其余部分将介绍如何通过系统调用 (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 的上次修改时间。

创建分页器和 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 页面

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

回写的页面状态

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

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

  1. 新提供的 zx_pager_supply_pages() 页面的开头为 Clean
  2. 当页面被写入时,它将转换为 Dirty。如果已使用 ZX_VMO_TRAP_DIRTY 创建了 VMO,则内核会先在确认用户分页器的 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第 1 版

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第 2 版

系统将添加新的系统调用 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 中那样捕获每个 Dirty 转换,但它只会增加未完成的脏页的数量(如果可以做到这一点而不超出分配的脏页限制)。如果新计数不超过该限制,内核将会继续写入,而不涉及用户分页器。这预计是正常操作模式,因此可以节省每次网页脏乱时往返用户分页器的费用。

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

  1. 分页器范围的脏限制
  2. 脏页时会计入此限制

对于 2),我们同样依赖于 ZX_VMO_TRAP_DIRTY VMO 创建标志。现在,此标志会触发生成一种新的寻呼机请求类型:ZX_VMO_DIRTY_MODE。现在,当内核执行 traps 写入时,它会查询文件系统,以确定是否应该选择将这些页面计入脏限制。用户分页器会返回 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 分页请求的费用。

这种设计可实现灵活的模型,其中文件系统可以在其 VMO 上混合使用不同类型的写入模式。以 CoW 模式写入的文件将通过 ZX_VMO_TRAP_DIRTY 创建其 VMO,并且其页面可以使用池化模式。同样,可以使用 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。对于已处于 Clean 状态的任何页面或处于 Dirty 状态的网页,系统会忽略该属性,并且其状态保持不变。

如果在执行刷新时(即在 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_BEGIN,可以选择将 data 设置为 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 中的选项字段设置为 ZX_VMO_DIRTY_RANGE_IS_ZERO,以表明它全部为零。

如果已使用 ZX_VMO_TRAP_DIRTY 标志创建了 VMO,并且将页面写入这个新扩展的范围,则内核会为它们生成 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 已分离,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 结构体将来可以扩展,包含用户分页器可能认为有用的更多字段。

modified 状态会在修改 VMO 的系统调用(如 zx_vmo_write()zx_vmo_set_size())上更新,还会在首次通过映射出现写入页面错误时更新。系统已跟踪页面上的首次写入错误,以正确管理从 CleanDirty 的转换,因此随后会更新 modified 状态。不过,系统不会跟踪通过脏页面上的映射传入的后续写入,因为这样做会显著降低向已映射 VMO 写入数据的速度。因此,映射的 VMO 的 modified 状态可能并不完全准确。

如果用户分页器还希望重置查询的统计信息,则 options 可以为 ZX_PAGER_RESET_VMO_STATS。值为 0 的 options 不会重置任何状态,只会执行纯查询。请注意,如果指定了 ZX_PAGER_RESET_VMO_STATS 选项,则此调用通过使用可查询状态来影响未来的 zx_pager_query_vmo_stats() 调用。例如,如果 zx_vmo_write() 后跟两个使用 ZX_PAGER_RESET_VMO_STATS 选项的连续 zx_pager_query_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 分页器请求解决此问题的方法,文件系统可以在刷新进行期间推迟处理。

早期技术和参考资料