RFC-0068:页面逐出提示

RFC-0068:页面驱逐提示
状态已接受
区域
  • 内核
说明

一种机制,用于让用户空间应用向内核暗示用户分页器支持的内存的相对驱逐顺序。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-01-08
审核日期(年-月-日)2021-02-10

摘要

本文档介绍了一种机制,供用户空间应用向内核提示用户分页器支持的内存的相对重要性,以便内核在决定在内存压力下要驱逐哪些页面时考虑这些提示。

设计初衷

Fuchsia 中的大多数可执行文件都由 blobfs 提供,blobfs 是一种不可变文件系统,它使用用户分页器按需从磁盘读取页面。通过用户分页器在内存中填充 Blob,我们可以在系统面临内存压力时逐出页面;当再次访问这些页面时,用户分页器只需将其重新读入即可。

不过,重新分页会带来性能开销:需要相应页面的线程必须阻塞,直到用户分页器处理页面故障(这涉及上下文切换和磁盘 IO)。为了尽量减少重新分页对性能的影响,内核使用最近最少使用的方案来查找要驱逐的页面。

这种驱逐方案并不总是最优的,有时可能会导致我们驱逐一段时间未访问但对性能至关重要的路径(例如音频堆栈)所需的页面。另一方面,我们还有一些页面不再受应用所需,并且可以安全地驱逐,例如没有任何客户端的“非活动”blob。这些非活跃网页会与当前有客户但一段时间未被访问的有效 Blob 中的网页混杂在一起。将这些非活跃页面移至队列前面以进行驱逐,对我们来说会很有帮助。

设计

概览

与内核相比,访问 blob 的用户空间应用以及提供这些 blob 的 blobfs 对页面的相对重要性有更多背景信息。它们可以将这些额外信息作为驱逐提示传递给内核。

此 RFC 提出了一个 API,可用于提供两种方向的提示:“优先考虑将这些页面驱逐出内存”和“保护这些页面免遭驱逐”。使用前面的示例,不活跃的 Blob 属于前者,而对性能至关重要的任务所需的页面属于后者。

Hinting API

  • zx_vmo_op_range() 系统调用将扩展为支持两个新操作。ZX_VMO_OP_DONT_NEED 提示指定范围已不再需要,应优先考虑将其驱逐。ZX_VMO_OP_ALWAYS_NEED 提示指定范围很重要,应尽可能保护该范围免遭驱逐。只有当系统因 OOM 而即将重新启动时,内核才会考虑驱逐标记有 ZX_VMO_OP_ALWAYS_NEED 提示的页面。(“替代方案”部分将详细介绍操作名称背后的动机。)

  • 同样,zx_vmar_op_range() 系统调用将扩展为支持操作 ZX_VMAR_OP_DONT_NEEDZX_VMAR_OP_ALWAYS_NEED

  • 这些提示仅适用于内核可以在内存压力下回收页面的 VMO 和映射,目前仅限由用户分页器支持的 VMO。随着未来添加更多可回收的 VMO 类型(例如可丢弃内存),提示 API 也可以扩展以涵盖这些类型。由于操作名称比较通用,因此未来可以扩展到并非严格从内存中驱逐页面的情况。例如,我们未来可能会考虑将提示 API 用于可压缩的匿名内存,其中回收只是内存压缩。

  • 对于不支持内核回收的 VMO 和映射,驱逐提示将无效。对通用匿名 VMO 或实体 VMO 进行提示不会改变其页面的提交或取消提交方式。这样一来,客户端便可轻松使用,无需预先确定 VMO 类型。由于此 API 仅用于传递提示,而无需对内核做出任何具体保证,因此内核可以选择在不适用的情况下忽略提示。

当前的驱逐策略

内核使用一组 4 个 LRU 页面队列跟踪在用户分页器支持的 VMO 中提交的页面。页面首次提交时,会先进入队列 1。网页扫描器每 10 秒旋转一次队列,将网页从 i 个队列移至 i+1 个队列。当系统面临内存压力时,内核会从第 4 个队列中逐出页面。每当访问任一队列中的页面时,该页面都会移至队列 1 的开头 - 队列 1 会跟踪最近使用过的页面。

OP_DONT_NEED

  • 我们将引入一个额外的 inactive 分页器队列,以按 LRU 顺序跟踪非活动页面。此队列不属于现有的 LRU 页面队列,因此页面扫描器不会将页面轮替到该队列中或从中轮替出页面。它仅按 LRU 顺序跟踪非活动页面,但不适用于所有由用户分页器支持的页面的更大 LRU 方案。

  • OP_DONT_NEED 会将指定范围内的所有已提交页面移至 inactive 队列。当内存压力开始出现时,内核会先考虑从 inactive 队列中逐出页面,然后再转到最早的 LRU 页面队列。

  • 如果访问 inactive 队列中的页面,该页面将从该队列移至第一个 LRU 页面队列,并失去之前指示的 OP_DONT_NEED 提示。从那时起,该页面将像任何其他“活跃”页面一样向下移动到其他 LRU 队列。这样可确保 OP_DONT_NEED 提示不会错误地替换网页的实际访问模式。

OP_ALWAYS_NEED

  • 系统会提交指定范围内的页面,并在相应的 vm_page_t 中设置新的 always_need 标志。

    • always_need 标志对页面扫描器轮替队列的方式没有任何影响。这有助于保留有关 always_need 标记的页面的活跃程度的信息,以便在需要驱逐该页面以防止 OOM 时,我们只驱逐最早的页面,从而防止内存回收过于干扰。

    • always_need 标志仅会影响在非 OOM 条件下通过驱逐移除页面的情况 - 当内核在内存压力下驱逐页面时,它只会跳过设置了此标志的页面。always_need 标志不会阻止通过非驱逐方式(例如取消提交、VMO 大小调整或 VMO 销毁)释放页面。

  • OP_ALWAYS_NEED 会将指定范围内的所有页面移至第一个 LRU 页面队列。无论如何,新提交的页面都会首先进入第一个 LRU 队列。对于已提交的页面,此操作会计为一次新访问,以使行为与新提交的页面保持一致。

  • 只有当内核尝试防止 OOM 时,才会考虑驱逐设置了 always_need 的页面。这种方法有助于我们实现良好的平衡,确保页面驱逐不会在系统正常运行期间影响性能,同时也不会锁定内存,从而提高 OOM 率。

与 VMO 克隆项互动

由于根 VMO 是唯一直接由分页器源支持的 VMO,因此提示操作仅适用于分页器支持的层次结构中的根 VMO 中的页面。克隆会从根 VMO 获取其初始内容。在克隆中提交的任何页面都是克隆拥有的分叉副本,无法被驱逐。因此,当在 VMO 克隆(或映射 VMO 克隆)上使用提示操作时,提示将应用于克隆在指定范围内可以看到的根 VMO 中的页面,即在克隆中尚未分叉的页面。

两个提示操作之间的互动

OP_ALWAYS_NEED 提示的优先级高于 OP_DONT_NEEDalways_need 标志是粘性的,一经设置便无法取消设置。这可防止来自克隆的 OP_DONT_NEED 替换其他克隆的 OP_ALWAYS_NEED

  • 根据上述操作的说明,紧随 OP_ALWAYS_NEED 之后的 OP_DONT_NEED 会将页面移至非活动队列,但由于设置了 always_need 标志,因此该页面不会被驱逐。

  • OP_DONT_NEED 后面的 OP_ALWAYS_NEED 会将页面从非活动页面队列移至第一个 LRU 队列,并设置 always_need 标志。

处理 op_range 系统调用的权限

提示操作不需要对 VMO / VMAR 句柄拥有任何特定权限;无论是否拥有权限,系统调用都会成功。不过,内核可以根据句柄权限、底层 VMO 类型、后备页来源、映射权限等组合,自由地忽略无意义的提示。换句话说,系统调用始终会成功,但在某些情况下,提示实际上可以视为无操作。

这种方法让我们可以灵活地进行实现,日后也更容易扩展到更多 VMO 类型。这也符合在不受支持的情况下将提示实现为无操作(而不是失败调用系统调用)背后的更大意图。客户端可以随时提供提示,内核将决定如何解读该提示,甚至可以选择忽略该提示。

与可丢弃内存的关系

可丢弃内存是用户空间影响内核内存回收策略的另一种方式。它允许客户端创建标记为“可丢弃”的匿名 VMO,并锁定和解锁这些 VMO,以分别指示它们何时处于使用状态或何时符合回收条件。虽然驱逐提示的用途与可丢弃内存类似(即向内核提供有关内存回收的更多信息),但二者存在一些关键差异。

  • 可丢弃内存仅适用于匿名 VMO。此 RFC 中提出的驱逐提示适用于有分页器支持的 VMO。

  • 与可丢弃的 VMO 搭配使用的锁定 / 解锁操作(zx_vmo_op_rangeZX_VMO_OP_LOCK/UNLOCK)具有更严格的语义。如果 VMO 处于锁定状态,则内核无法舍弃其页面。另一方面,即使 VMO 的页面被标记为 OP_ALWAYS_NEED,如果内存压力条件非常严峻,内核也可以选择将其驱逐。这是因为 VMO 由分页器支持,并且可以按需重新填充已舍弃的页面。

  • 锁定可以被视为与提示完全不同的操作;这两者不能互换,但可以共存。如果将来扩展了驱逐提示以支持可丢弃内存,锁定仍将继续成为客户端指示 VMO 何时处于使用状态的手段,禁止内核丢弃 VMO。然后,只需在适用时在其上叠加驱逐提示,即可表示回收的相对优先级顺序 - 标记为 OP_DONT_NEED 且处于未锁定的可丢弃 VMO 会在标记为 OP_ALWAYS_NEED 且处于未锁定的可丢弃 VMO 之前被丢弃。

实现

这是新 API,因此目前没有依赖项。内核端实现可以单独完成。实现该 API 后,用户空间客户端就可以开始采用它了。

性能

OP_ALWAYS_NEED 会改进当前在被驱逐页面中分页会对用户产生明显影响的路径的性能。目前已知的一个用例是驱逐后播放音频,这有时会因分页活动而导致故障。

OP_DONT_NEED 会导致指定页面更快被驱逐,从而导致这些页面在稍后分页时出现延迟。不过,这是预期行为,客户应该知道这一影响。此外,这些网页今天可能也已被驱逐。OP_DONT_NEED 旨在用于明确指明不会被访问的非活跃网页,使其符合被驱逐的条件。

这些提示操作旨在使内存回收系统更为稳健,这样被逐出的页面就更有可能保持被逐出状态更长时间。因此,系统的内存运行状况应该会有所改善。

安全注意事项

无。

隐私注意事项

无。

测试

  • 通过多个 VMO 克隆来测试新 API 的核心测试 / 单元测试。
  • 用于验证内核端驱逐行为的单元测试。

文档

需要更新 Zircon 系统调用文档,以添加新 API。

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

锁定,而不是 OP_ALWAYS_NEED

zx_vmo_op_range()ZX_VMO_OP_LOCK 搭配使用可以替代使用 ZX_VMO_OP_ALWAYS_NEED。不过,内核需要通过锁定提供更强的保证:已提交的页面需要固定在内存中,以防止内核在其未解锁之前回收它们。这可能会给系统施加额外的内存压力,导致系统更快地发生 OOM。

确定要锁定的确切网页也可能非常具有挑战性。在大多数情况下,客户端可能会采取更为保守的做法,锁定的网页数量会超出所需。这可能会产生高昂的内存开销,如果极端化,可能会削弱最初采用需求分页的内存优势。

特定于驱逐的操作名称

我们可以使用更具体的操作名称(例如 ZX_VMO_OP_EVICT_FIRST/LASTZX_VMO_OP_RECLAIM_FIRST/LAST),这些名称可以更精确地描述关联的驱逐行为。不过,这些功能无法扩展到未来的更通用应用。RECLAIM_FIRST/LAST 可能比 EVICT_FIRST/LAST 的范围更广,可以应用于“回收”的各种定义,例如内存压缩,它不会严格驱逐页面,但仍会将操作与内存回收概念相关联。

借助 OP_ALWAYS_NEEDOP_DONT_NEED,我们可以更好地捕获用户 intent,而不是定义预期要发生的关联内核操作。这样一来,日后便可更灵活地解读这些操作。这样一来,我们还可以保留与开发者可能熟悉的 madvise 的一些相似之处。

提示的潜在滥用

客户端可能会无意中或故意滥用该 API,通过不受限制地使用 OP_ALWAYS_NEED 来尝试阻止大量网页被驱逐。虽然系统仍会回收这部分内存以防止 OOM,但在其他时候,这部分内存可能会给系统带来额外的内存压力。

不过,这种风险与客户创建大型 VMO 并提交其所有网页的情况类似。未来,一旦我们制定了一些关于控制一般内存用量(例如空间银行)的政策,就可以将相同的政策馈送到提示逻辑中。

将 API 纯粹建模为提示还可让内核灵活地在需要时忽略提示。我们可以采取一些措施,例如忽略标记了 always_need 的网页上超出一定限制的 OP_ALWAYS_NEED 提示,或者仅在驱逐期间跳过网页一定次数。在极端情况下,我们还可以通过将提示操作转换为无操作来完全废弃提示操作。

后续工作

如前所述,除了由用户分页器支持的 VMO 之外,提示操作还可扩展到其他用例。其他类型的可回收内存(例如可丢弃内存)似乎是自然延伸。我们还可以将其扩展到通用匿名内存,并使用它来驱动严格驱逐以外的操作。

随着虚拟机系统变得更加成熟,OP_DONT_NEEDOP_ALWAYS_NEED 之间的互动(以及 always_need 标志是否粘性)未来可能会发生变化。目前的选择是受当前用例驱动的实现简化,而不是提示 API 本身的基本要求。

OP_DONT_NEEDOP_ALWAYS_NEED 为中点 OP_WILL_NEED 预留了空间,该中点可用作预提取提示,表示将来需要某个范围,但不一定需要在该点之后受到驱逐保护。用户分页器可以使用此提示来预读页面。

在先技术和参考文档

在 Linux 上,madvise 支持 MADV_WILLNEEDMADV_DONTNEED