Fuchsia 的内存回收

大多数操作系统都采用内存回收策略,以确保在任何时间点正在运行的进程工作集都可以高效地利用所有可用的物理内存。操作系统将具有固定数量的物理内存 (RAM) 分布在所有正在运行的进程中,可能无法同时容纳所有进程。

简单来说,内存回收就是页面替换,即对当前用户活动不那么重要的页面被替换为可能更重要的页面。大多数操作系统都会维护一个可用页面池,以便快速实现传入的内存分配,而不会在等待释放正在使用的页面时阻塞。

Fuchsia 还采用了类似的策略,在策略中,系统会尝试使可用内存量大于特定阈值。Fuchsia 在内核和用户空间内使用了多种内存回收技术。本指南介绍了这些内存回收技术的运作方式。Fuchsia 还提供了一组用于分析和转储内存使用情况的工具(请参阅内存使用情况分析工具)。

分页器支持的内存逐出

用户空间文件系统使用分页器从外部来源(如磁盘)按需对文件进行分页。文件系统使用 VMO 表示内存中的文件,其页面在被访问时由分页器服务填充。

在 Fuchsia 上,blobfs 是一个不可变文件系统,它使用分页器按需填充页面来托管所有可执行文件。当系统内存不足时,即可用可用内存开始不足,内核会逐出由 blobfs 支持的页面以回收内存。由于这些页面存在于磁盘上,因此可以在需要时重新提取。

内核会跟踪所有由分页器支持的内存。当可用内存不足时,它会寻找适合逐出的候选对象。系统会在多个 LRU(最近最少使用)页面队列中跟踪网页,内核后台线程会定期轮替这些页面,以便对页面进行“老化”处理。另一个后台线程会在内存紧张时从最旧的页面队列中逐出页面。

随着可用内存下降的下降,内核会调整其老化和逐出政策以采取更积极的措施,加快页面的老化速度以找到更多可逐出的候选。为了防止出现抖动,MRU(最近使用)页面队列中的页面永远不会被逐出。此队列的长度取决于系统的流失量。

如果系统相对安静且内存使用量稳定,则内核会减慢页面老化速度,并在 MRU 队列中累积更多页面。另一方面,如果用户循环执行多个 activity,不断切换工作集,则内核会尝试更积极地老化页面来跟上这种情况。

用户空间进程还可以使用逐出提示来影响内核逐出策略。进程可以使用 DONT_NEED 提示来指示不再使用的页面,并且这些页面很适合逐出。它们还可以使用 ALWAYS_NEED 来指示页面很重要且不应被逐出,从而避免在有用户再次访问它们时将其重新提取。

如需详细了解逐出提示,请参阅以下参考文档:zx_vmo_op_rangezx_vmar_op_range

零页面重复信息删除

匿名 VMO(无分页器支持)中的页面仅在写入时填充 / 提交。内核使用单例零页面执行读取。即使在写入时提交页面后,内核也会尝试删除重复信息中仅填充了零的页面,以便返回到单例零页面,以节省内存。内核会定期扫描匿名 VMO 中的物理页面,以寻找机会删除重复的零页面。

页面表回收

地址空间中所述,VMAR 层次结构有助于内核跟踪虚拟到物理内存的映射。用户首次访问某个虚拟地址时,系统会使用该地址空间的 VMAR 树来查找底层物理页面。虚拟到物理的映射随后存储在硬件页面表中,MMU 会在将来执行查询时使用。在内存紧张时,内核会回收已有一段时间无人访问的硬件页面表中的内存。当再次需要这些映射时,可以根据 VMAR 树重建它们。

可舍弃的 VMO

用户空间进程可以创建可舍弃的 VMO 的特殊变种。客户端可以锁定和解锁 可舍弃的 VMO,具体取决于它们是否正在使用。当系统内存紧张时,内核会找到已解锁的可舍弃 VMO 并将其释放。

示例代码(模数错误处理):

// Create a discardable VMO.
zx_handle_t vmo;
uint64_t vmo_size = 5 * zx_system_get_page_size();
zx_vmo_create(vmo_size, ZX_VMO_DISCARDABLE, &vmo);

// Lock the VMO.
zx_vmo_lock_state_t lock_state = {};
zx_vmo_op_range(vmo, ZX_VMO_OP_LOCK, 0, vmo_size, &lock_state,
                sizeof(lock_state));

// Use the VMO as desired.
zx_vmo_read(vmo, buf, 0, sizeof(buf));

// Unlock the VMO. The kernel is free to discard it now.
zx_vmo_op_range(vmo, ZX_VMO_OP_UNLOCK, 0, vmo_size, nullptr, 0);

// Lock the VMO again before use.
zx_vmo_op_range(vmo, ZX_VMO_OP_LOCK, 0, vmo_size, &lock_state,
                sizeof(lock_state));

if (lock_state.discarded_size > 0) {
  // The kernel discarded the VMO. Re-initialize it if required.
  zx_vmo_write(vmo, data, 0, sizeof(data));
} else {
  // The kernel did not discard the VMO. Previous contents were preserved.
}

内存压力信号

Fuchsia 为用户空间进程提供了直接控制其内存消耗的功能,以响应系统级可用内存。客户端可以注册以接收内存压力信号,并根据观察到的内存压力水平执行操作。内存压力等级分为三种

名称说明
1

内存压力水平正常。

已注册的客户端可随意保留缓存并不受限制地分配内存。

但是,客户端应注意不要在转换为 NORMAL 级别时主动重新创建缓存,从而导致内存峰值立即再次将级别推入 WARNING。

2

内存压力水平受到一定限制,如果未选中,可能会越过临界压力范围。

已注册的客户端应优化其操作以限制内存用量,而不是通过减少缓存大小和非必需的内存分配等实现最佳性能。

客户端必须注意控制为回收内存而执行的工作量,并确保这不会造成明显的性能下降。虽然存在一些内存压力,但这不足以证明需要牺牲用户响应能力来回收内存。

3

内存压力水平非常有限。

已注册的客户端应丢弃所有非必需的内存,并避免分配更多内存。否则,可能会导致作业终止,或者在出现全局内存压力时重新启动系统。

如果需要,客户端可能会执行成本高昂的工作来回收内存,因为如果未能这样做可能会导致终止。在这种情况下,客户端可能会认为性能命中是合理的权衡。

将内存压力信号与可舍弃的 VMO 进行比较

用户空间客户端可以在内存压力信号和可舍弃的 VMO 之间进行选择,也可以根据需要结合使用这两种回收机制。在做出选择时,请考虑以下事项:

  • 内存压力信号可让客户端做的不仅仅是精简缓存。例如,作业可以拆解其作业树中的非必需进程。 它们还可以停止某些内存密集型活动,或者推迟启动新的活动,直到压力水平为“正常”。
  • 使用可舍弃的 VMO 时,用户空间客户端会放弃控制何时将内存释放给内核。内核会根据各种因素确定何时释放内存:可用内存量、可通过其他方式回收的内存等。如果客户端希望精细控制其缓存的生命周期、何时缩减内容等,则内存压力信号可能更合适。
  • 与进程因响应内存压力信号而拆解 VMO 本身相比,可舍弃的 VMO 最终保留其内容的时间可能会更长。内核会推动可舍弃的 VMO 的释放,并且内核提供有关可用内存量的更多全局上下文,因此它确切知道要回收多少内存。内核还可以通过其他方式回收内存,因此可能并非所有可舍弃的 VMO 都需要释放。另一方面,如果用户空间客户端本身应对内存压力,则它可能会每次都以相同的方式做出响应,缩减其所有缓存。
  • 可舍弃内存还可以让内核更快地回收内存,从而加快系统恢复速度。对于内存压力信号,在从内核发出压力等级变化的信号到用户空间进程对其做出响应之间,可能会涉及一些 IPC 和调度延迟。

OOM(内存不足)重新启动

面对某些激进的内存分配模式,所有内存回收策略都可能无法释放足够的内存。当发生这种情况时,内核会选择在彻底关闭文件系统后重新启动,以防止数据丢失。当可用内存级别低于预配置的 OOM 阈值时,会触发 OOM 重新启动。

测试内存压力响应的工具

观察和测试内核内存回收

使用 k scanner 命令观察和测试内核使用的回收技术:由分页器支持的逐出、可舍弃的 VMO 回收、零页面重复信息删除和页表回收。它还可用于测试用于推动逐出的页面队列旋转 / 老化策略。在串行控制台上运行 k scanner 以查看所有可用选项:

k scanner
usage:
scanner dump                    : dump scanner info
scanner push_disable            : increase scanner disable count
scanner pop_disable             : decrease scanner disable count
scanner reclaim_all             : attempt to reclaim all possible memory
scanner rotate_queue            : immediately rotate the page queues
scanner reclaim <MB> [only_old] : attempt to reclaim requested MB of memory.
scanner pt_reclaim [on|off]     : turn unused page table reclamation on or off
scanner harvest_accessed        : harvest all page accessed information

k scanner dump 会转储页面队列的当前状态以及内核用于回收的其他相关内存计数器:

k scanner dump
[SCAN]: Scanner enabled. Triggering informational scan
[SCAN]: Found 4303 zero pages across all of memory
[SCAN]: Found 8995 user-pager backed pages in queue 0
[SCAN]: Found 3278 user-pager backed pages in queue 1
[SCAN]: Found 8947 user-pager backed pages in queue 2
[SCAN]: Found 10776 user-pager backed pages in queue 3
[SCAN]: Found 3981 user-pager backed pages in queue 4
[SCAN]: Found 0 user-pager backed pages in queue 5
[SCAN]: Found 0 user-pager backed pages in queue 6
[SCAN]: Found 0 user-pager backed pages in queue 7
[SCAN]: Found 1347 user-pager backed pages in DontNeed queue
[SCAN]: Found 40 zero forked pages
[SCAN]: Found 0 locked pages in discardable vmos
[SCAN]: Found 0 unlocked pages in discardable vmos
pq: MRU generation is 12 set 10.720698018s ago due to "Active ratio", LRU generation is 6
pq: Pager buckets [8995],[3278],8947,10776,3981,0,{0},0, evict first: 1347, live active/inactive totals: 12273/25051

使用 k scanner reclaimk scanner reclaim_all 测试回收内存:

k scanner reclaim_all
[EVICT]: Free memory before eviction was 7161MB and after eviction is 7290MB
[EVICT]: Evicted 33004 user pager backed pages
[SCAN]: De-duped 25 pages that were recently forked from the zero page

使用 k pmm drop_user_pt 测试页面表回收:

k pmm
…
pmm drop_user_pt                             : drop all user hardware page tables

观察和产生内存压力

使用 k pmm mem_avail_state 命令通过分配内存达到指定的内存压力水平,对系统产生内存压力。这对于测试整个系统对内存压力的响应非常有用:

k pmm mem_avail_state
pmm mem_avail_state info                     : dump memory availability state info
pmm mem_avail_state [step] <state> [<nsecs>] : allocate memory to go to memstate <state>, hold the state for <nsecs> (10s by default). Only works if going to <state> from current state requires allocating memory, can't free up pre-allocated memory. In optional [step] mode, allocation pauses for 1 second at each intermediate memory availability state until <state> is reached.

k pmm mem_avail_state info 转储当前的内存压力状态。

k pmm mem_avail_state info
watermarks: [50M, 60M, 150M, 300M]
debounce: 1M
current state: 4
current bounds: [299M, 16.0E]
free memory: 7253.5M

内存可用性状态从 0 开始编号,是之前针对内存压力信号提到的级别的超集。

  • OOM 是状态 0。这是可用内存级别,低于此值,内核会决定重新启动系统。
  • Imminent-OOM 是状态 1。这是一个仅用于诊断的内存级别,设置在 OOM 级别之上的较小增量。其唯一目的是提供一种安全收集 OOM 诊断信息的方法,因为现在收集 OOM 级别的诊断可能为时已晚。如需详细了解此级别,请参阅 RFC-0091
  • Critical 是状态 2。该级别会触发关键内存压力信号。
  • Warning 是状态 3。该级别会触发 WARNING 内存压力信号。
  • Normal 是状态 4。该电平会触发 NORMAL 内存压力信号。

在上面的示例中,current state 为 4,即普通。

watermarks 显示内存阈值,用于描述不同的内存可用性状态。上例中的输出显示了这些内存阈值:

OOM: 50MB, Imminent-OOM: 60MB, Critical: 150MB, Warning: 300MB

debounce 是在计算内存状态边界时使用的空闲或误差范围。在本示例中,大小为 1MB。

current bounds 显示适用于当前内存状态的可用内存边界。假设当前状态为 Normal(引用 watermarks),Normal 从 300MB 阈值开始。如果使用 1 MB 去抖动,则下限为 299 MB。Normal 级别没有适用的上限,它在此处设置为 UINT64_MAX

最后,系统上的总 free memory 为 7253.5MB。

使用 k pmm mem_avail_state X 命令转换为内存可用性状态 X,其中 X 是上述数字内存状态。(可选)提供将保留请求状态的时长。还有一个选项可用来“单步调试”中间状态,并在每种状态处暂停。

例如,这会触发向 Critical 内存状态的转换:

k pmm mem_avail_state 2
memory-pressure: memory availability state - Critical
pq: MRU generation is 714 set 4.144414945s ago due to "Active ratio", LRU generation is 708
pq: Pager buckets [3482],[115],317,0,199,0,{6939},0, evict first: 0, live active/inactive totals: 3597/7455
memory-pressure: set target memory to evict 1MB (free memory is 149MB)
Leaked 1817528 pages
Sleeping for 10 seconds...
[EVICT]: Free memory before eviction was 147MB and after eviction is 151MB
[EVICT]: Evicted 986 user pager backed pages
Freed 1817528 pages
memory-pressure: memory availability state - Normal
pq: MRU generation is 717 set 1.213355379s ago due to "Timeout", LRU generation is 711
pq: Pager buckets [4351],[258],149,37,0,1,{5798},0, evict first: 0, live active/inactive totals: 4609/5985

在本例中,系统通过分配 1817528 个页面(页面大小为 4KB)转换到 Critical。然后休眠 10 秒(默认用于保持状态),在此期间 Critical 压力持续存在。最后,1817528 分配的页面被释放,内存压力下降到 NormalCritical 状态转换也导致一些由分页器支持的内存被逐出,如 [EVICT] 行所示。

k pmm mem_avail_state 命令对于测试整个系统的内存压力响应非常有用。由于它通过分配实际物理内存来工作,因此会在内核内和用户空间内运用系统可自行处理的所有回收机制。

这些是额外的 k pmm oom 命令,专门用于在 OOM 级别测试系统响应。

pmm oom [<rate>]                             : leak memory until oom is triggered, optionally specify the rate at which to leak (in MB per second)
pmm oom hard                                 : leak memory aggressively and keep on leaking
pmm oom signal                               : trigger oom signal without leaking memory

使用 k pmm oom 的示例输出如下:

k pmm oom
Disabling VM scanner
memory-pressure: free memory is 49MB, evicting pages to prevent OOM...
pq: MRU generation is 13 set 7.979442243s ago due to "Active ratio", LRU generation is 7
pq: Pager buckets [4538],[4517],3624,4606,13716,4976,{0},0, evict first: 1347, live active/inactive totals: 9055/28269
memory-pressure: found no pages to evict
memory-pressure: free memory after OOM eviction is 49MB
…
memory-pressure: pausing for 8s after OOM mem signal
[00028.317] 02811:03481> [fshost] INFO: [admin-server.cc(33)] received shutdown command over admin interface
[00028.317] 02811:03481> [fshost] INFO: [fs-manager.cc(281)] filesystem shutdown initiated
[00028.317] 02811:38032> [fshost] INFO: [fs-manager.cc(310)] Shutting down /data
[00028.318] 12900:12902> [minfs] INFO: [minfs.cc(1471)] Shutting down
[00028.340] 12900:12902> [minfs] WARNING: [src/storage/minfs/bin/main.cc(53)] Unmounted
[00028.341] 02811:03481> [fshost] INFO: [admin-server.cc(39)] shutdown complete
[00028.342] 02811:02813> [fshost] INFO: [main.cc(309)] terminating
[00028.342] 02687:02689> [driver_manager.cm] INFO: [suspend_handler.cc(205)] Successfully waited for VFS exit completion
memory-pressure: rebooting due to OOM
memory-pressure: stowing crashlog
ZIRCON REBOOT REASON (OOM)
Shutting down debuglog
platform_halt suggested_action 1 reason 3
Rebooting...

模拟用户空间中的内存压力信号

使用 ffx profile memory signal 命令模拟用户空间中的内存压力信号,而不会造成实际的内存压力。如果目标是测试特定用户空间进程对内存压力信号的响应,而不改变系统的内存状态,这会非常有用。

Signals userspace clients with specified memory pressure level. Clients can use this
command to test their response to memory pressure. Does not affect the real memory
pressure level on the system, or trigger any kernel reclamation tasks.
Positional Arguments:
  level             memory pressure level. Can be CRITICAL, WARNING or NORMAL.

例如,使用 ffx profile memory signal WARNING 时,ffx log 输出中会显示以下内容:

[00213.059579][26701][26703][memory_monitor] INFO: [pressure_notifier.cc:106] Simulating memory pressure level WARNING

请注意,此命令实际上不会分配任何内存。它只是模拟用户空间中请求级别的一次性内存压力信号,而不会影响内核的内存可用性状态。因此,它不会触发任何内核内存回收,例如逐出分页器支持的内存。