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
文件系统条目中公开的 USS 和 PSS 值。
我们只会提出对 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 中提出的更改。
要求
- 内核的内存归因实现不得通过使 VMO 层次结构锁定更精细来阻止优化 Zircon 中的锁争用。
- 当
memory_monitor
等工具对系统中所有 VMO 的归因进行求和时,总和必须与系统的实际物理内存用量一致。这是“总和为 1”属性。 - Starnix 必须能够在 Zircon 提供的 API 之上模拟 Linux 内存归因 API。这包括在相关的 proc 文件系统条目中提供 USS、RSS 和 PSS 测量结果以及其他测量结果。
设计
我们提议的更改会影响 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 求和可测量系统总内存用量。
我们选择通过公开所有 USS、RSS 和 PSS 来更改归因 API,因为这是我们探索的替代方案中唯一满足所有要求的选项。
对用户空间的影响
此提案继续保留现有用户空间工具依赖的“总和为 1”属性。在这种新行为下,归因查询可能会产生不同的结果,但对系统中所有 VMO 的查询进行求和仍可提供准确的内存用量统计数据。这些更改不会导致 memory_monitor
和 ps
等程序开始过量或过少统计内存,事实上,它们会使这些程序的每个 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_VMO
和zx_info_vmo_t
,并更改 所有版本zx_info_vmo_t
中的两个现有字段的含义:
- 我们将通过以下方式保留 ABI 和 API 向后兼容性:
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
系列结构体,因此对这些结构体所做的所有更改都适用于本主题。
- 本主题会重复使用
- 我们将通过以下方式保留 ABI 和 API 向后兼容性:
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_pages
和populated_pages
是一项破坏性更改。请参阅下文中的“向后兼容性”。
- 重命名
- 我们将添加新的
ZX_INFO_PROCESS_MAPS
、zx_info_maps_t
和zx_info_maps_mapping_t
,并更改现有版本zx_info_maps_mapping_t
中的两个字段的含义。新版结构体中不存在以下字段(见下文):
- 我们将通过以下方式保持 ABI 向后兼容性:
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_STATS
和zx_info_task_stats_t
,并更改zx_info_task_stats_t
结构体所有版本中三个现有字段的含义:
- 我们将通过以下方式保留 ABI 和 API 向后兼容性:
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_read
、zx_vmo_write
、zx_vmo_op_range
和 zx_vmo_transfer_data
)的性能。目前,这些操作会争用由所有相关 VMO 共享的层次结构锁,该锁会对相关 VMO 克隆中的所有这些操作进行序列化。对于多次克隆的 VMO,序列化性能会更差。许多 Starnix 和 bootfs VMO 都属于此类别。
Zircon 在创建和销毁子 VMO 时会执行更多工作。创建 SNAPSHOT
和 SNAPSHOT_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_pages
和 populated_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 计算 USS、RSS 或 PSS 测量所需的信息。
仅公开 USS 和 RSS
在此选项中,我们会更改内核,将共享的写时复制页面归因于共享这些页面的每个 VMO,并分别显示私有页面和共享页面的归因。
它与精细的层次锁定兼容。
不过,它无法为用户空间提供足够的信息来保留“总和为 1”属性,也无法让 Starnix 计算 PSS 测量值。内核会使用此选项多次归因 COW 共享页面。每个 COW 共享页面的共享次数可能会因写入模式而异,因此用户空间需要按页提供信息,以便删除 COW 共享页面的重复内容,或在克隆之间平均分配这些页面。
公开每个 VMO 树的详细信息
在此选项中,我们会更改内核,将共享的写时复制页面归因于共享这些页面的每个 VMO,并公开每个树和每个 VMO 的信息:
- 用于将 VMO 与其树相关联的树标识符
- 用于决胜负的每个 VMO 的稳定标识符,例如时间戳
- 每个 VMO 的私享网页数
- 每个树的共享网页总数
- 每个 VMO 可见的每个树共享网页的数量
它为用户空间提供满足“总和为 1”属性所需的信息。
不过,它会阻止我们进行锁争用优化,因为它需要现有的共享层次结构锁才能正常运行。如果没有这样的共享锁,计算“每个树的共享页面总数”是不可行的。此选项还会将内核实现细节公开给用户空间,这会使日后维护虚拟机代码变得更加困难。它也不提供 Starnix 计算 PSS 测量所需的信息。
在先技术和参考文档
其他操作系统提供本 RFC 中使用的 RSS、USS 和 PSS 测量值,但有时使用不同的名称。Windows、MacOS 和 Linux 均会跟踪 RSS 和 USS。基于 Linux 的操作系统会跟踪 PSS。
Linux 通过 proc 文件系统公开这些测量结果。ps 和 top 等归因工具旨在以用户友好的方式呈现这些信息。