RFC-0068:页面逐出提示

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

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

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

摘要

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

设计初衷

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

不过,重新分页会带来性能成本 - 需要页面的线程必须阻塞,直到用户分页器处理页面错误,这涉及上下文切换和磁盘 IO。为了尽量减少重分页对性能的影响,内核使用最近最少使用方案来查找要逐出的页面。

这种逐出方案并不总是最佳的,有时会导致我们逐出一段时间内未被访问但在性能关键路径(例如音频堆栈)上必需的页面。另一方面,有些页面已不再为应用所必需,可以安全地逐出,例如没有任何客户端的“非活跃”Blob。这些非活跃网页会与来自活跃 blob 的网页混在一起,这些活跃 blob 目前有客户端,但只是有一段时间未被访问。将这些不活跃的网页移到逐出队列的前面,有助于提高性能。

设计

概览

访问 blob 的用户空间应用和提供这些 blob 的 blobfs 比内核更了解网页的相对重要性。它们可以将这些额外信息作为逐出提示传递给内核。

此 RFC 提出了一种 API,可用于提供两个方向的提示:“优先考虑驱逐这些页面”和“保护这些页面免遭驱逐”。根据前面的示例,不活跃的 blob 属于前一类,而性能关键型任务所需的网页属于后一类。

Hinting API

  • 将扩展 zx_vmo_op_range() 系统调用以支持两个新操作。 ZX_VMO_OP_DONT_NEED 表示不再需要指定的范围,应优先考虑将其逐出。ZX_VMO_OP_ALWAYS_NEED 表示指定范围很重要,应尽可能长时间地防止其被逐出。仅当系统因内存不足而即将重新启动时,内核才会考虑逐出标记为 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 队列开始。对于已提交的网页,此操作会被视为新的访问,以保持与新提交的网页一致的行为。

  • 如果页面设置了 always_need,则只有在内核尝试防止 OOM 时,才会考虑将其逐出。这种方法有助于我们实现良好的平衡,确保页面逐出不会在系统的正常运行期间影响性能,同时也不会锁定内存,从而提高 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 类型。这也与将提示实现为针对不支持的情况的 no-op 而不是使系统调用失败的更大意图相符。客户端始终可以提供提示,内核将决定如何解读该提示,甚至可以选择忽略它。

与可舍弃内存的关系

可舍弃内存是用户空间影响内核内存回收策略的另一种方式。它允许客户端创建标记为“可丢弃”的匿名 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) 的情况。

确定要锁定哪些网页也可能非常困难。在大多数情况下,客户可能会更加保守,锁定比所需更多的网页。这种做法的内存开销可能过高,如果采用极端做法,可能会在很大程度上削弱按需分页带来的内存优势。

逐出特定操作名称

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

OP_ALWAYS_NEEDOP_DONT_NEED 使我们能够更好地捕获用户意图,而不是定义预期会发生的关联内核操作。这样一来,未来就可以更灵活地解读这些操作。此外,它还允许我们保留与开发者可能熟悉的 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