RFC-0254:更改写入时复制网页的归因

RFC-0254:更改写入时复制网页的归因
状态已接受
区域
  • 内核
说明

更改了 Zircon 公开的用于归因写时复制页面的 API 和语义。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2024-08-02
审核日期(年-月-日)2024-07-29

问题陈述

Zircon 当前用于将内存归因于 VMO 的行为在以下两方面对 Starnix 内核造成了问题:

首先,Zircon 当前的内存归因行为仅与在 Zircon 的 VMO 实现中使用广泛共享的锁相兼容。在运行多个 Starnix 进程时,此共享锁是导致某些性能问题的原因。

Starnix 使用 VMO 来支持每个 MAP_ANONYMOUS Linux 映射,并在 Linux 进程进行 fork() 调用时在该进程中克隆所有此类 VMO。由于有许多 Linux 进程,因此会导致许多 VMO 争用一小组共享锁。在我们对一项可节省内存的优化进行原型设计时,Starnix 会为进程中的所有映射使用一个大型 VMO,共享锁会退化为所有 Starnix VMO 争用的一个“全局”锁。

其次,Starnix 无法使用 Zircon 提供的 API 来模拟 Linux 内存归因的确切行为。它们不会区分 VMO 中的私有写时复制页面和共享写时复制页面,因此也不会显示任何此类共享页面的共享次数。Starnix 需要这些信息才能准确计算各种 /proc 文件系统条目中公开的 USSPSS 值。

我们只会提出对 Zircon 的内存归因 API 和行为的更改,这些更改将解决这些问题,同时保留依赖于这些 API 和行为的工具的现有功能集。本文不打算解决在用户空间实体(例如组件和运行程序)之间分配内存的更一般问题。我们也不打算将这些更改视为归因的“终极解决方案” - 它们只是解决 Starnix 目前面临的特定性能瓶颈和功能缺口的手段。

摘要

改进了 Zircon 的内存归因 API 和行为,以考虑共享的写时复制页面。

利益相关方

教员

neelsa@google.com

Reviewers:

  • adanis@google.com
  • jamesr@google.com
  • etiennej@google.com
  • wez@google.com

咨询了

  • maniscalco@google.com
  • davemoore@google.com

社交

我们在与 Zircon、Starnix 和内存团队的利益相关方沟通时,就此 RFC 进行了讨论。利益相关方还审核了一份设计文档,其中概述了此 RFC 中提出的更改。

要求

  1. 内核的内存归因实现不得通过使 VMO 层次结构锁定更精细来阻止优化 Zircon 中的锁争用。
  2. memory_monitor 等工具对系统中所有 VMO 的归因进行求和时,总和必须与系统的实际物理内存用量一致。这是“总和为 1”属性。
  3. Starnix 必须能够在 Zircon 提供的 API 之上模拟 Linux 内存归因 API。这包括在相关的 proc 文件系统条目中提供 USSRSSPSS 测量结果以及其他测量结果。

设计

我们提议的更改会影响 Zircon 如何通过写时复制将页面分配给 VMO 克隆之间共享的页面。这些是 COW 私有页面和 COW 共享页面。

我们不建议更改 Zircon 如何通过内存映射或重复的 VMO 句柄将页面分配给进程共享。这些是进程私有页面和进程共享页面。

用户空间负责在适当情况下将进程共享页面归因于一个或多个进程。Zircon 通常不会在决策过程中考虑进程共享,但有一个例外情况。如需了解详情,请参阅“向后兼容性”。

共享类型是相互独立的 - 给定页面可以是 COW 私有、COW 共享、进程私有和进程共享的任意组合。

Zircon 的现有行为

Zircon 的现有归因行为会为每个 VMO 提供单次衡量结果:归因于 VMO 的 COW 私有页面和 COW 共享页面中的内存,每个页面都有唯一的归因。

  • 它会将 COW 共享页面唯一归因于可以引用它们的最早活跃 VMO。有时,兄弟 VMO 之间会共享 COW 共享页面,因此引用某个页面的最早活跃 VMO 不必是其他 VMO 的父级。
  • 对系统中的所有 VMO 求和可测量系统总内存用量。
  • 它不会衡量 VMO 的 USS,因为其中可能包含 部分 COW 共享页面。
  • 它也不会衡量 VMO 的 RSS,因为它可能不包含 所有 COW 共享的网页。
  • 它不会衡量 VMO 的 PSS,因为该衡量会根据每个网页的共享次数来衡量其贡献。

Zircon 的新行为

Zircon 的新归因行为会提供以下三个 VMO 级衡量结果:

  • USS:来自 VMO 的 COW 私有页面的归因内存。这种衡量方式会将网页仅归因于该 VMO。它用于衡量销毁 VMO 时会释放的内存。
  • RSS:来自 VMO 的 COW 私有页面和 COW 共享页面的归因内存。此衡量方法会将 COW 共享网页归因于共享这些网页的每个 VMO,因此会多次统计 COW 共享网页。它用于衡量 VMO 引用的内存总量。
  • PSS:来自 VMO 的 COW 私有页面和 COW 共享页面的归因内存。此衡量标准会在共享相应网页的所有 VMO 之间均匀分配每个 COW 共享网页的归因。对于给定的 VMO,它可以是字节的部分数,因为每个 VMO 都会获得其每个共享页面的字节数的等分。它用于衡量 VMO 的内存“比例”用量。对系统中的所有 VMO 求和可测量系统总内存用量。

我们选择通过公开所有 USSRSSPSS 来更改归因 API,因为这是我们探索的替代方案中唯一满足所有要求的选项。

对用户空间的影响

此提案继续保留现有用户空间工具依赖的“总和为 1”属性。在这种新行为下,归因查询可能会产生不同的结果,但对系统中所有 VMO 的查询进行求和仍可提供准确的内存用量统计数据。这些更改不会导致 memory_monitorps 等程序开始过量或过少统计内存,事实上,它们会使这些程序的每个 VMO 和每个任务的统计结果更准确。

新 API 会公开 PSS 的字节值小数,并且用户空间必须选择启用才能观察这些字节值小数。_fractional_scaled 字段包含部分字节,除非用户空间使用这些字节,否则会略微低估 PSS

Starnix 目前会低估 Linux 映射的 RSS 值,因为当前的归因行为只会统计 COW 共享页面一次。RSS 应多次统计共享的网页。在没有 Zircon 帮助的情况下,Starnix 无法模拟此行为,因此我们更改了 Zircon 归因 API,以提供正确的 RSS 值。我们可以通过另一轮 API 更改来实现这一点,但为了避免流失用户,我们选择将这些更改批量处理。

memory_monitor 会将 VMO 视为与引用其任何子项的进程共享,即使是未直接引用 VMO 的进程也是如此。它会在这些进程之间平均分配父 VMO 的归属内存。这会尝试考虑 COW 共享页面,并允许 memory_monitor 的“总和为 1”,但可能会导致一些错误结果。例如,它可以将父 VMO 的一部分 COW 私有页面分配给不引用该 VMO 且因此无法引用这些页面的过程。此外,当父级 COW 共享页面与部分子页面共享,但与其他子页面不共享时,系统也会错误地缩放父级页面。我们的变更使此行为变得多余,因此我们将将其移除。当多个进程直接引用 VMO 时,该工具仍会扩缩 VMO 的内存。如需了解详情,请参阅“向后兼容性”。

Syscall API 变更

Zircon 的归因 API 包含 zx_object_get_info 主题,我们将对其进行更改:

  • ZX_INFO_VMO
    • 我们将通过以下方式保留 ABI 和 API 向后兼容性:
      • ZX_INFO_VMO 重命名为 ZX_INFO_VMO_V3,同时保留其值。
      • 已将 zx_info_vmo_t 重命名为 zx_info_vmo_v3_t
    • 我们将添加新的 ZX_INFO_VMOzx_info_vmo_t,并更改 所有版本 zx_info_vmo_t 中的两个现有字段的含义:
typedef struct zx_info_vmo {
  // Existing and unchanged `zx_info_vmo_t` fields omitted for brevity.

  // These fields already exist but change meaning.
  //
  // These fields include both private and shared pages accessible by a VMO.
  // This is the RSS for the VMO.
  //
  // Prior versions of these fields assigned any copy-on-write pages shared by
  // multiple VMO clones to only one of the VMOs, making the fields inadequate
  // for computing RSS. If 2 VMO clones shared a single page then queries on
  // those VMOs would count either `zx_system_get_page_size()` or 0 bytes in
  // these fields depending on the VMO queried.
  //
  // These fields now include all copy-on-write pages accessible by a VMO. In
  // the above example queries on both VMO's count `zx_system_get_page_size()`
  // bytes in these fields. Queries on related VMO clones will count any shared
  // copy-on-write pages multiple times.
  //
  // In all other respects these fields are the same as before.
  uint64_t committed_bytes;
  uint64_t populated_bytes;

  // These are new fields.
  //
  // These fields include only private pages accessible by a VMO and not by any
  // related clones. This is the USS for the VMO.
  //
  // These fields are defined iff `committed_bytes` and `populated_bytes` are,
  // and they are the same re: the definition of committed vs. populated.
  uint64_t committed_private_bytes;
  uint64_t populated_private_bytes;

  // These are new fields.
  //
  // These fields include both private and shared copy-on-write page that a VMO
  // can access, with each shared page's contribution scaled by how many VMOs
  // can access that page. This is the PSS for the VMO.
  //
  // The PSS values may contain fractional bytes, which are included in the
  // "fractional_" fields. These fields are fixed-point counters with 63-bits
  // of precision, where 0x800... represents a full byte. Users may accumulate
  // these fractional bytes and count a full byte when the sum is 0x800... or
  // greater.
  //
  // These fields are defined iff `committed_bytes` and `populated_bytes` are,
  // and they are the same re: the definition of committed vs. populated.
  uint64_t committed_scaled_bytes;
  uint64_t populated_scaled_bytes;
  uint64_t committed_fractional_scaled_bytes;
  uint64_t populated_fractional_scaled_bytes;
} zx_info_vmo_t;
  • ZX_INFO_PROCESS_VMOS
    • 我们将通过以下方式保留 ABI 和 API 向后兼容性:
      • ZX_INFO_PROCESS_VMOS 重命名为 ZX_INFO_PROCESS_VMOS_V3,同时保留其值。
    • 我们将添加一个新的 ZX_INFO_PROCESS_VMOS
      • 本主题会重复使用 zx_info_vmo_t 系列结构体,因此对这些结构体所做的所有更改都适用于本主题。
  • ZX_INFO_PROCESS_MAPS
    • 我们将通过以下方式保持 ABI 向后兼容性:
      • ZX_INFO_PROCESS_MAPS 重命名为 ZX_INFO_PROCESS_MAPS_V2,同时保留其值。
      • 已将 zx_info_maps_t 重命名为 zx_info_maps_v2_t
      • 已将 zx_info_maps_mapping_t 重命名为 zx_info_maps_mapping_v2_t
    • 在以下情况下,我们会破坏 API 兼容性:
      • 重命名 committed_pagespopulated_pages 是一项破坏性更改。请参阅下文中的“向后兼容性”。
    • 我们将添加新的 ZX_INFO_PROCESS_MAPSzx_info_maps_tzx_info_maps_mapping_t,并更改现有版本 zx_info_maps_mapping_t 中的两个字段的含义。新版结构体中不存在以下字段(见下文):
typedef struct zx_info_maps {
  // No changes, but is required to reference the new `zx_info_maps_mapping_t`.
} zx_info_maps_t;

typedef struct zx_info_maps_mapping {
  // Existing and unchanged `zx_info_maps_mapping_t` fields omitted for brevity.

  // These fields are present in older versions of `zx_info_maps_mapping_t` but
  // not this new version. In the older versions they change meaning.
  //
  // See `committed_bytes` and `populated_bytes` in the `zx_info_vmo_t` struct.
  // These fields change meaning in the same way.
  uint64_t committed_pages;
  uint64_t populated_pages;

  // These are new fields which replace `committed_pages` and `populated_pages`
  // in this new version of `zx_info_maps_mapping_t`.
  //
  // These fields are defined in the same way as the ones in `zx_info_vmo_t`.
  uint64_t committed_bytes;
  uint64_t populated_bytes;

  // These are new fields.
  //
  // These fields are defined in the same way as the ones in `zx_info_vmo_t`.
  uint64_t committed_private_bytes;
  uint64_t populated_private_bytes;
  uint64_t committed_scaled_bytes;
  uint64_t populated_scaled_bytes;
  uint64_t committed_fractional_scaled_bytes;
  uint64_t populated_fractional_scaled_bytes;
} zx_info_maps_mapping_t;
  • ZX_INFO_TASK_STATS
    • 我们将通过以下方式保留 ABI 和 API 向后兼容性:
      • ZX_INFO_TASK_STATS 重命名为 ZX_INFO_TASK_STATS_V1,同时保留其值。
      • 已将 zx_info_task_stats_t 重命名为 zx_info_task_stats_v1_t
    • 我们将添加新的 ZX_INFO_TASK_STATSzx_info_task_stats_t,并更改 zx_info_task_stats_t 结构体所有版本中三个现有字段的含义:
typedef struct zx_info_task_stats {
  // These fields already exist but change meaning.
  //
  // These fields include either private or shared pages accessible by mappings
  // in a task.
  // `mem_private_bytes` is the USS for the task.
  // `mem_private_bytes + mem_shared_bytes` is the RSS for the task.
  // `mem_private_bytes + mem_scaled_shared_bytes` is the PSS for the task.
  //
  // Prior versions of these fields only considered pages to be shared when they
  // were mapped into multiple address spaces. They could incorrectly attribute
  // shared copy-on-write pages as "private".
  //
  // They now consider pages to be shared if they are shared via either multiple
  // address space mappings or copy-on-write.
  //
  // `mem_private_bytes` contains only pages which are truly private - only one
  // VMO can access the pages and that VMO is mapped into one address space.
  //
  // `mem_shared_bytes` and `mem_scaled_shared_bytes` contain all shared pages
  // regardless of how they are shared.
  //
  // `mem_scaled_shared_bytes` scales the shared pages it encounters in two
  // steps: first each page is scaled by how many VMOs share that page via
  // copy-on-write, then each page is scaled by how many address spaces map the
  // VMO in the mapping currently being considered.
  //
  // For example, consider a single page shared between 2 VMOs P and C.
  //
  // If P is mapped into task p1 and C is mapped into tasks p2 and p3:
  // `mem_private_bytes` will be 0 for all 3 tasks.
  // `mem_shared_bytes` will be `zx_system_get_page_size()` for all 3 tasks.
  // `mem_scaled_shared_bytes` will be `zx_system_get_page_size() / 2` for p1
  // and `zx_system_get_page_size() / 4` for both p2 and p3.
  //
  // If P is mapped into task p1 and C is mapped into tasks p1 and p2:
  // `mem_private_bytes` will be 0 for both tasks.
  // `mem_shared_bytes` will be `2 * zx_system_get_page_size()` for p1 and
  // `zx_system_get_page_size()` for p2.
  // `mem_scaled_shared_bytes` will be `3 * zx_system_get_page_size() / 4` for
  // p1 and `zx_system_get_page_size() / 4` for p2.
  uint64_t mem_private_bytes;
  uint64_t mem_shared_bytes;
  uint64_t mem_scaled_shared_bytes;

  // This is a new field.
  //
  // This field is defined in the same way as the "_fractional" fields in
  // `zx_info_vmo_t`.
  uint64_t mem_fractional_scaled_shared_bytes;
} zx_info_task_stats_t;

实现

我们将结构化内核 API 更改(例如添加和重命名字段)作为单个初始 CL 来实现,以减少流失。我们将所有新字段的值设置为 0,但 _fractional_scaled 字段除外,我们将其设置为标记值 UINT64_MAX

接下来,我们将更改 memory_monitor 在将父 VMO 内存归因于进程以及使用 PSS _scaled_bytes 字段(而非 RSS _bytes 字段)方面的行为。_fractional_scaled 字段中的哨兵值目前会控制这两种向后不兼容的行为。此门控以及其他新字段中的 0 值表示用户空间行为不会发生任何变化。

然后,我们将从内核中公开新的归因行为。此更改将改变 committed_bytes 等现有字段的含义,并且新字段可以采用非零值。_fractional_scaled 字段无法再采用标记值,因此用户空间将自动开始使用新行为和新字段来利用此更改。

最后,我们将从 memory_monitor 中移除对 _fractional_scaled 字段的检查。

我们更改的 API 的旧版本会被视为已废弃。当我们确信没有任何二进制文件再引用这些文件时,可以稍后将其移除。

性能

新的归因实现方式将提升归因查询的效果。它会减少 VMO 克隆的处理次数,并避免执行用于在多个 VMO 之间对共享 COW 页面进行解析的昂贵检查。

之后,实现精细的层次锁定将提高页面故障和多个 VMO 系统调用(包括 zx_vmo_readzx_vmo_writezx_vmo_op_rangezx_vmo_transfer_data)的性能。目前,这些操作会争用由所有相关 VMO 共享的层次结构锁,该锁会对相关 VMO 克隆中的所有这些操作进行序列化。对于多次克隆的 VMO,序列化性能会更差。许多 Starnix 和 bootfs VMO 都属于此类别。

Zircon 在创建和销毁子 VMO 时会执行更多工作。创建 SNAPSHOTSNAPSHOT_MODIFIED 子项会变为 O(#parent_pages),而不是 O(1)。在所有情况下,销毁任何类型的子项都会变为 O(#parent_pages),而之前在某些情况下可能会变为 O(#child_pages)。Zircon 目前会将此工作推迟到更频繁的操作(例如页面故障)。将其移至不太频繁的 zx_vmo_create_child()zx_vmo_destroy() 操作,会使其他频繁的操作在交换时更快。因此,我们预计不会出现任何用户可见的性能回归问题。

我们将使用现有的微基准和宏基准来验证预期的性能增量。

向后兼容性

ABI 和 API 兼容性

我们将为所有内核 API 变更创建其他版本化主题,并保留对所有现有主题的支持。这将在所有情况下保留 ABI 兼容性,并在所有情况下(一个例外)保留 API 兼容性。

zx_info_maps_mapping_t 中重命名 committed_pagespopulated_pages 是一项破坏 API 的更改。这些字段仅在 Fuchsia 内部工具中的少数位置使用,因此重命名这些字段的 CL 也会更改所有调用点中的名称。

除非要利用新添加的字段,否则其他更改不需要更新现有调用点。

行为兼容性

Starnix、ps 和 Fuchsia 外部的几个库会计算每个 VMO 或每个任务的 RSS 值。由于会多次统计 COW 共享的网页,因此新归因行为会使这些计算更准确。这将以向后不兼容的方式更改 RSS 值,但只会使其更准确,因此我们预计不会产生任何负面影响。

memory_monitor 为父级 VMO 分配内存的方法存在正确性问题,而新分配行为可以解决此问题。我们必须以不向后兼容的方式更改此工具的实现,才能实现修复。为避免流失,我们将在实现新的归因行为之前,将新的 _fractional_scaled 字段设为值为 UINT64_MAX 的哨兵值。我们将根据此值控制 memory_monitor 的实现。归因通常不会生成此标志值,因此它非常适合作为“功能标志”。

我们在 ZX_INFO_TASK_STATS 中将“共享”的含义保留为“进程共享”,尽管它的运作方式与其他归因 API 不同。这样,依赖于 ZX_INFO_TASK_STATS 行为的现有程序便可继续运行,而无需进行侵入性更改。未来的工作可能会将对 ZX_INFO_TASK_STATS 的这些用法替换为其他查询(可能为 memory_monitor),届时我们可能会废弃并移除 ZX_INFO_TASK_STATS

测试

核心测试套件中已经包含许多用于验证内存归因的测试。

我们将添加一些新测试,以验证与新的归因 API 和行为相关的极端情况。

文档

我们将更新 zx_object_get_info 的系统调用文档,以反映这些更改。

我们将在 docs/concepts/memory 下添加一个新页面,其中介绍了 Zircon 的内存归因 API 并提供了示例。

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

在制定此提案的过程中,我们探索了一些替代解决方案模型。其中大多数方法易于实现,但具有不理想的性能或可维护性特征。

模拟当前归因行为

在此选项中,我们将更改内核,以便在新的实现之上模拟现有的归因行为。

它没有向后兼容性问题,也不需要进行任何 API 更改。

不过,它会阻止使用精细的层次结构锁定,因为它需要现有的共享层次结构锁定才能正常运行。在更精细的锁定策略下,此模拟会导致归因查询死锁。这还会导致归因查询的性能变得更差;目前,归因查询的性能已经令人担忧。最后,它不提供 Starnix 计算 USSRSSPSS 测量所需的信息。

仅公开 USS 和 RSS

在此选项中,我们会更改内核,将共享的写时复制页面归因于共享这些页面的每个 VMO,并分别显示私有页面和共享页面的归因。

它与精细的层次锁定兼容。

不过,它无法为用户空间提供足够的信息来保留“总和为 1”属性,也无法让 Starnix 计算 PSS 测量值。内核会使用此选项多次归因 COW 共享页面。每个 COW 共享页面的共享次数可能会因写入模式而异,因此用户空间需要按页提供信息,以便删除 COW 共享页面的重复内容,或在克隆之间平均分配这些页面。

公开每个 VMO 树的详细信息

在此选项中,我们会更改内核,将共享的写时复制页面归因于共享这些页面的每个 VMO,并公开每个树和每个 VMO 的信息:

  • 用于将 VMO 与其树相关联的树标识符
  • 用于决胜负的每个 VMO 的稳定标识符,例如时间戳
  • 每个 VMO 的私享网页数
  • 每个树的共享网页总数
  • 每个 VMO 可见的每个树共享网页的数量

它为用户空间提供满足“总和为 1”属性所需的信息。

不过,它会阻止我们进行锁争用优化,因为它需要现有的共享层次结构锁才能正常运行。如果没有这样的共享锁,计算“每个树的共享页面总数”是不可行的。此选项还会将内核实现细节公开给用户空间,这会使日后维护虚拟机代码变得更加困难。它也不提供 Starnix 计算 PSS 测量所需的信息。

在先技术和参考文档

其他操作系统提供本 RFC 中使用的 RSSUSSPSS 测量值,但有时使用不同的名称。Windows、MacOS 和 Linux 均会跟踪 RSSUSS。基于 Linux 的操作系统会跟踪 PSS

Linux 通过 proc 文件系统公开这些测量结果。ps 和 top 等归因工具旨在以用户友好的方式呈现这些信息。