RFC-0068:页面逐出提示

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

用户空间应用向内核提示与用户分页器支持的内存的相对逐出顺序的机制。

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

总结

此 RFC 描述了一种机制,可让用户空间应用提示内核用户分页器支持内存的相对重要性,以便内核在决定在内存紧张时逐出哪些页面时考虑这些提示。

设计初衷

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

但是,重新分页会降低性能,需要页面的线程必须阻塞,直到用户分页器处理页面故障,这涉及上下文切换和磁盘 IO。为了尝试降低回收对性能的影响,内核会使用最近最少使用的架构来查找要逐出的页面。

这种逐出方案并非总是最佳的,并且有时可能会导致我们逐出一段时间内无人访问但性能关键路径(例如音频堆栈)上必须访问的网页。另一方面,应用不再需要并且可以安全逐出页面,例如没有任何客户端的“非活动”blob。这些非活跃页面会与活跃 blob 中的页面混合在一起,这些 blob 目前具有客户端,但有段时间只是未被访问过。不妨将这些不活跃的网页 移到最前沿进行逐出

设计

概览

访问 Blob 的用户空间应用以及提供这些 Blob 的 blobfs 在页面相对重要性方面的上下文比内核多。它们可以将这些额外的信息作为逐出提示传递给内核。

此 RFC 提出了一个 API,该 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 中克隆可以在指定范围内看到的页面,即尚未在克隆中派生的页面。

两个提示操作之间的交互

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 类型。在不受支持的情况下,将提示实现为空操作,而不是使系统调用失败,这也符合更大的 intent。客户端可以始终提示 - 内核将决定如何解读该提示,甚至可以选择忽略该提示。

与可舍弃内存的关系

可舍弃内存是用户空间影响内核内存回收策略的另一种方式。它允许客户端创建标记为“可舍弃”并处于锁定和解锁状态的匿名 VMO,以分别指示它们何时正在使用中或可以回收。虽然逐出提示与可丢弃内存具有类似的目标,即为内核提供有关内存回收的更多信息,但两者仍存在一些关键区别。

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

  • 与可舍弃的 VMO 一起使用的锁定 / 解锁操作(zx_vmo_op_rangeZX_VMO_OP_LOCK/UNLOCK)具有更严格的语义。如果 VMO 被锁定,则内核无法舍弃其页面。另一方面,即使 VMO 的页面被标记为 OP_ALWAYS_NEED,如果内存压力情况很严重,内核仍然可以选择逐出它们。这是因为 VMO 由分页器支持,舍弃的页面可以按需重新填充。

  • 锁定可以视为与提示完全独立的操作;两者不可互换,并且可以共存。如果将来扩展逐出提示以支持可舍弃内存,则锁定仍然是客户端指明何时正在使用 VMO 的方式,从而禁止内核舍弃它。然后,逐出提示可以仅叠加在顶层,以表示回收的相对优先级顺序(仅在适用 - 已解锁的可舍弃 VMO 带有 OP_DONT_NEED 标记时,可以先于带有 OP_ALWAYS_NEED 标记的内容被舍弃)。

实现

这是一个新 API,因此在此阶段没有依赖项。内核端实现可以单独完成。实现该 API 后,用户空间客户端便可开始采用它。

性能

在逐出页面中的分页目前会对用户造成可观察的影响时,OP_ALWAYS_NEED 可提高路径性能。目前,一个已知的用例是在逐出后播放音频,这有时会由于分页 Activity 而导致故障。

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