不可驱逐 LRU 基础设施

简介

本文档描述了 Linux 内存管理器的 “不可驱逐 LRU” 基础设施,以及如何使用它来管理几种类型的 “不可驱逐” folio。

本文档试图提供此机制背后的总体原理,以及驱动实现的一些设计决策的原理。后者的设计原理将在实现描述的上下文中进行讨论。诚然,人们可以通过阅读代码来获得实现细节 - “它做什么?”。希望下面的描述通过提供 “它为什么这样做?” 的答案来增加价值。

不可驱逐 LRU

不可驱逐 LRU 功能添加了一个额外的 LRU 列表,用于跟踪不可驱逐的 folio,并将这些 folio 从 vmscan 中隐藏。此机制基于 Red Hat 的 Larry Woodman 的一个补丁,旨在解决 Linux 中 folio 回收的几个可扩展性问题。这些问题已在客户站点的大内存 x86_64 系统上观察到。

为了用一个例子来说明这一点,一个具有 128GB 主内存的非 NUMA x86_64 平台将在单个节点中拥有超过 3200 万个 4k 页面。当很大一部分这些页面因任何原因都不可驱逐 [见下文] 时,vmscan 将花费大量时间扫描 LRU 列表,寻找一小部分可驱逐的页面。这可能会导致所有 CPU 花费 100% 的时间在 vmscan 中,持续数小时甚至数天,系统完全无响应。

不可驱逐列表处理以下几类不可驱逐的页面:

  • ramfs 拥有的页面。

  • 使用 noswap 挂载选项的 tmpfs 拥有的页面。

  • 映射到 SHM_LOCK'd 共享内存区域的页面。

  • 映射到 VM_LOCKED [mlock()ed] VMA 的页面。

在未来,该基础设施可能还能够处理其他使页面不可驱逐的情况,无论是根据定义还是根据具体情况。

不可驱逐 LRU Folio 列表

不可驱逐 LRU folio 列表是一个谎言。它从来不是一个 LRU 排序的列表,而是 LRU 排序的匿名和文件、活动和非活动 folio 列表的伴随列表;现在它甚至不是一个 folio 列表。但是按照熟悉的惯例,在本文档和源代码中,我们经常将其想象为第五个 LRU folio 列表。

不可驱逐 LRU 基础设施由一个额外的、每个节点的 LRU 列表(称为 “不可驱逐” 列表)和一个关联的 folio 标志 PG_unevictable 组成,用于指示该 folio 正在不可驱逐列表上进行管理。

PG_unevictable 标志类似于 PG_active 标志,并且与 PG_active 标志互斥,因为它指示当设置了 PG_lru 时,folio 驻留在哪个 LRU 列表上。

不可驱逐 LRU 基础设施将不可驱逐的 folio 维护得如同它们在额外的 LRU 列表上一样,原因如下:

  1. 我们可以 “像处理系统中其他 folio 一样处理不可驱逐的 folio - 这意味着我们可以使用相同的代码来操作它们,相同的代码来隔离它们(用于迁移等),相同的代码来跟踪统计信息等等...” [Rik van Riel]

  2. 我们希望能够在节点之间迁移不可驱逐的 folio,以进行内存碎片整理、工作负载管理和内存热插拔。Linux 内核只能迁移那些可以成功从 LRU 列表隔离的 folio(或 “可移动” 的 folio:此处不予考虑)。如果我们在 LRU 式列表以外的其他地方维护 folio,而这些 folio 可以被 folio_isolate_lru() 检测到,我们将阻止它们的迁移。

不可驱逐列表不区分文件支持的 folio 和匿名、交换支持的 folio。这种区分仅在 folio 实际上可驱逐时才重要。

不可驱逐列表受益于 Christoph Lameter 最初提出并发布的每个节点 LRU 列表和统计信息的 “数组化”。

内存控制组交互

不可驱逐 LRU 功能通过扩展 lru_list 枚举与内存控制组 [又名内存控制器;请参阅 内存资源控制器] 交互。

由于每个节点 LRU 列表的 “数组化” (每个 lru_list 枚举元素一个),内存控制器数据结构会自动获得每个节点的不可驱逐列表。内存控制器跟踪页面进出不可驱逐列表的移动。

当内存控制组面临内存压力时,控制器不会尝试回收不可驱逐列表上的页面。这会产生几个影响:

  1. 由于页面在不可驱逐列表中被 “隐藏” 起来,不会被回收,因此回收过程可以更有效,仅处理有可能被回收的页面。

  2. 另一方面,如果分配给控制组的页面中有太多是不可驱逐的,则控制组中任务的工作集的驱逐部分可能不适合可用内存。这可能导致控制组抖动或 OOM 杀死任务。

标记地址空间为不可驱逐

对于 ramfs 等设施,附加到地址空间的任何页面都不能被驱逐。为了防止任何此类页面被驱逐,提供了 AS_UNEVICTABLE 地址空间标志,文件系统可以使用许多包装函数来操作它:

  • void mapping_set_unevictable(struct address_space *mapping);

    将地址空间标记为完全不可驱逐。

  • void mapping_clear_unevictable(struct address_space *mapping);

    将地址空间标记为可驱逐。

  • int mapping_unevictable(struct address_space *mapping);

    查询地址空间,如果它完全不可驱逐,则返回 true。

这些目前在内核中的三个地方使用:

  1. ramfs 在创建其 inode 时标记其 inode 的地址空间,并且此标记在 inode 的生命周期内保持不变。

  2. SYSV SHM 标记 SHM_LOCK'd 地址空间,直到调用 SHM_UNLOCK。请注意,如果锁定的页面被换出,则 SHM_LOCK 不需要将锁定的页面调入;如果应用程序想要确保它们在内存中,则必须手动访问这些页面。

  3. i915 驱动程序标记固定的地址空间,直到它被取消固定。 i915 驱动程序标记的不可驱逐内存量大致等于 debugfs/dri/0/i915_gem_objects 中的绑定对象大小。

检测不可驱逐页面

mm/internal.h 中的函数 folio_evictable() 使用上面概述的查询函数 [请参阅 标记地址空间为不可驱逐] 来检查 AS_UNEVICTABLE 标志,以确定 folio 是否可驱逐。

对于在填充后被标记的地址空间(如 SHM 区域可能的情况),锁定操作(例如 SHM_LOCK)可以是懒惰的,不需要像 mlock() 那样填充该区域的页表,也不需要进行任何特殊努力将 SHM_LOCK'd 区域中的任何页面推送到不可驱逐列表。相反,如果 vmscan 在回收扫描期间遇到这些 folio,它将执行此操作。

在解锁操作(如 SHM_UNLOCK)时,解锁器(例如 shmctl())必须扫描该区域中的页面,并从不可驱逐列表中 “营救” 它们,如果没有其他条件使它们保持不可驱逐状态。如果销毁了不可驱逐的区域,则在释放页面的过程中,也会从不可驱逐列表中 “营救” 这些页面。

folio_evictable() 还通过调用 folio_test_mlocked() 来检查 mlocked 的 folio,当一个 folio 被错误地引入到 VM_LOCKED VMA 中,或在 VM_LOCKED 的 VMA 中找到时,会设置该标志。

Vmscan 对不可驱逐 Folio 的处理

如果不可驱逐的页在缺页处理路径中被剔除,或者在 mlock() 或 mmap() 时被移动到不可驱逐列表,那么 vmscan 将不会遇到这些页,直到它们再次变得可驱逐(例如通过 munlock())并从不可驱逐列表中被“拯救”出来。然而,在某些情况下,为了方便起见,我们可能会决定将一个不可驱逐的页保留在常规的活跃/非活跃 LRU 列表之一上,让 vmscan 来处理。vmscan 会在所有的 shrink_{active|inactive|folio}_list() 函数中检查这类页,并且会“剔除”它遇到的这类页:也就是说,它会将这些页转移到正在扫描的内存 cgroup 和节点的不可驱逐列表中。

在某些情况下,一个页可能被映射到 VM_LOCKED VMA 中,但是该页没有设置 mlocked 标志。这样的页会一路到达 shrink_active_list() 或 shrink_folio_list(),在那里,当 vmscan 在 folio_referenced()try_to_unmap() 中遍历反向映射时,它们会被检测到。当页被 shrinker 释放时,它会被剔除到不可驱逐列表中。

要“剔除”一个不可驱逐的页,vmscan 只是在放弃页锁之后,使用 folio_putback_lru() 将页放回 LRU 列表 —— 这是 folio_isolate_lru() 的反向操作。由于使页不可驱逐的条件在页解锁后可能会发生变化,__pagevec_lru_add_fn() 会在将页放置到不可驱逐列表之前重新检查页的不可驱逐状态。

MLOCKED 页

除了 ramfs 和 SYSV SHM 之外,不可驱逐页列表对于 mlock() 也很有用。请注意,mlock() 仅在 CONFIG_MMU=y 的情况下可用;在 NOMMU 的情况下,所有映射实际上都被 mlocked。

历史

“不可驱逐的 mlocked 页”基础设施基于 Nick Piggin 最初发布的 RFC 补丁中的工作,该补丁标题为“mm: mlocked pages off LRU”。Nick 发布他的补丁是为了替代 Christoph Lameter 发布的补丁,以实现相同的目标:将 mlocked 页从 vmscan 中隐藏起来。

在 Nick 的补丁中,他使用 struct page LRU 列表链接字段之一作为映射该页面的 VM_LOCKED VMA 的计数(Rik van Riel 在三年前也有相同的想法)。但是,这种将链接字段用于计数的方式阻止了对 LRU 列表上的页面的管理,因此 mlocked 页无法迁移,因为 folio_isolate_lru() 无法检测到它们,并且 LRU 列表链接字段对于迁移子系统不可用。

Nick 通过在尝试隔离 mlocked 页之前将其放回 LRU 列表来解决这个问题,从而放弃了 VM_LOCKED VMA 的计数。当 Nick 的补丁与不可驱逐的 LRU 工作集成时,该计数被在 munlocking 时遍历反向映射所取代,以确定是否还有其他 VM_LOCKED VMA 映射该页面。

然而,当许多 mlocked 页面的进程试图退出时,在 munlocking 时为每个页面遍历反向映射是很丑陋且低效的,并且可能导致文件 rmap 锁上的灾难性争用。在 5.18 版本中,在不可驱逐 LRU 列表链接字段中保留 mlock_count 的想法被重新提出并付诸实践,而不会阻止 mlocked 页的迁移。这就是为什么现在的“不可驱逐 LRU 列表”不能是页面的链表的原因;但是无论如何都没有用到该链表 —— 尽管其大小是为了 meminfo 而维护的。

基本管理

mlocked 页 - 映射到 VM_LOCKED VMA 中的页 - 是一类不可驱逐的页。当内存管理子系统“注意到”这样一个页时,该页会被标记为 PG_mlocked 标志。这可以使用 folio_set_mlocked() 和 folio_clear_mlocked() 函数来操作。

当 PG_mlocked 页被添加到 LRU 时,它将被放置在不可驱逐列表上。内存管理可以在多个地方“注意到”这样的页

  1. 在 mlock()/mlock2()/mlockall() 系统调用处理程序中;

  2. 在 mmap() 系统调用处理程序中,当使用 MAP_LOCKED 标志映射区域时;

  3. 在调用了带有 MCL_FUTURE 标志的 mlockall() 的任务中映射一个区域时;

  4. 在缺页处理路径中,以及当 VM_LOCKED 堆栈段扩展时;或者

  5. 如上所述,在 vmscan:shrink_folio_list() 中,当尝试通过 folio_referenced()try_to_unmap() 回收 VM_LOCKED VMA 中的页面时。

mlocked 页在以下情况下会被解锁并从不可驱逐列表中拯救出来

  1. 在通过 munlock()/munlockall() 系统调用解锁的范围内映射时;

  2. 当页面从最后一个映射它的 VM_LOCKED VMA 中 munmap() 时,包括在任务退出时取消映射;

  3. 当页面从 mmapped 文件的最后一个 VM_LOCKED VMA 中截断时;或者

  4. 在 VM_LOCKED VMA 中页面被 COW 之前。

mlock()/mlock2()/mlockall() 系统调用处理

mlock()、mlock2() 和 mlockall() 系统调用处理程序会为调用指定的范围内的每个 VMA 继续执行 mlock_fixup()。对于 mlockall(),这是任务的整个活动地址空间。请注意,mlock_fixup() 用于 mlocking 和 munlocking 内存范围。调用 mlock() 一个已经 VM_LOCKED 的 VMA,或者调用 munlock() 一个不是 VM_LOCKED 的 VMA,都被视为无操作,mlock_fixup() 只是返回。

如果 VMA 通过了下面“过滤特殊 VMA”中描述的一些过滤,mlock_fixup() 将尝试将其与邻居合并,或者如果该范围没有覆盖整个 VMA,则拆分出 VMA 的一个子集。然后,通过 mlock_vma_pages_range(),通过 walk_page_range(),通过 mlock_pte_range(),所有已存在于 VMA 中的页面都会通过 mlock_folio() 标记为 mlocked。

在从系统调用返回之前,do_mlock() 或 mlockall() 将调用 __mm_populate(),通过 get_user_pages() 缺页调入剩余的页面,并将这些页面标记为 mlocked。

请注意,被 mlocked 的 VMA 可能使用 PROT_NONE 映射。在这种情况下,get_user_pages() 将无法缺页调入页面。这没关系。如果页面最终被缺页调入此 VM_LOCKED VMA,它们将在缺页处理路径中处理 - 这也是 mlock2() 的 MLOCK_ONFAULT 区域的处理方式。

对于每个被缺页调入 VMA 的 PTE (或 PMD),页面添加 rmap 函数会调用 mlock_vma_folio(),当 VMA 是 VM_LOCKED 时(除非它是透明巨页的一部分的 PTE 映射),该函数会调用 mlock_folio()。或者,当它是新分配的匿名页面时,folio_add_lru_vma() 会调用 mlock_new_folio() 代替:类似于 mlock_folio(),但可以做出更好的判断,因为此页面是独占的,并且已知尚未在 LRU 上。

mlock_folio() 立即设置 PG_mlocked,然后将页面放置在 CPU 的 mlock folio 批处理中,以批量处理其余的工作,这些工作将在 lru_lock 下由 __mlock_folio() 完成。__mlock_folio() 设置 PG_unevictable,初始化 mlock_count,并将页面移动到不可驱逐状态(“不可驱逐的 LRU”,但是使用 mlock_count 代替 LRU 线程)。或者,如果页面已经是 PG_lru、PG_unevictable 和 PG_mlocked,它只是递增 mlock_count。

但是,在实践中,这可能无法理想地工作:页面可能尚未在 LRU 上,或者可能已暂时从 LRU 中隔离。在这种情况下,无法触及 mlock_count 字段,但稍后当 __munlock_folio() 将页面返回到“LRU”时,它将被设置为 0。竞争阻止 mlock_count 在那时被设置为 1:为了避免永久性地将页面滞留在不可驱逐状态,总是将 mlock_count 设置得较低,这样当 munlocked 时,页面将被拯救到可驱逐的 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,则可能稍后再次被 mlocked。

过滤特殊 VMA

mlock_fixup() 过滤了几类“特殊”VMA

  1. 设置了 VM_IO 或 VM_PFNMAP 的 VMA 将被完全跳过。这些映射后面的页面本质上是被固定的,因此我们不需要将它们标记为 mlocked。在任何情况下,大多数页面都没有 struct page 可以用来标记该页。因此,get_user_pages() 将对这些 VMA 失败,因此尝试访问它们没有任何意义。

  2. 映射 hugetlbfs 页面的 VMA 已经有效地固定在内存中。我们既不需要也不想 mlock() 这些页面。但是 __mm_populate() 包括 hugetlbfs 范围,分配大页面并填充 PTE。

  3. 带有 VM_DONTEXPAND 的 VMA 通常是内核页面的用户空间映射,例如 VDSO 页面、中继通道页面等。这些页面本质上是不可驱逐的,并且不受 LRU 列表管理。__mm_populate() 包括这些范围,如果尚未填充,则填充 PTE。

  4. 设置了 VM_MIXEDMAP 的 VMA 不会被标记为 VM_LOCKED,但是 __mm_populate() 包括这些范围,如果尚未填充,则填充 PTE。

请注意,对于所有这些特殊的 VMA,mlock_fixup() 都没有设置 VM_LOCKED 标志。因此,我们不必在稍后的 munlock()、munmap() 或任务退出期间处理它们。mlock_fixup() 也没有将这些 VMA 计入任务的“locked_vm”。

munlock()/munlockall() 系统调用处理

munlock() 和 munlockall() 系统调用由与 mlock()、mlock2() 和 mlockall() 系统调用相同的 mlock_fixup() 函数处理。如果调用 munlock 一个已经 munlocked 的 VMA,mlock_fixup() 只是返回。由于上面讨论的 VMA 过滤,VM_LOCKED 不会在任何“特殊”VMA 中设置。因此,这些 VMA 将在 munlock 中被忽略。

如果 VMA 是 VM_LOCKED,mlock_fixup() 会再次尝试合并或拆分指定的范围。然后,通过 mlock_vma_pages_range(),通过 walk_page_range(),通过 mlock_pte_range(),VMA 中的所有页面都会通过 munlock_folio() 被 munlocked - 这是 mlocking VMA 范围时使用的相同函数,但带有指示正在执行 munlock() 的新标志。

munlock_folio() 使用 mlock pagevec 批量处理工作,这些工作将在 lru_lock 下由 __munlock_folio() 完成。__munlock_folio() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 mlocked 标志并清除不可驱逐标志,将页面从不可驱逐状态移动到非活动 LRU。

但是,在实践中,这可能无法理想地工作:页面可能尚未到达“不可驱逐的 LRU”,或者可能已暂时从中隔离。在这些情况下,其 mlock_count 字段是不可用的,必须假定为 0:这样该页面将被拯救到可驱逐的 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,则可能稍后再次被 mlocked。

迁移 MLOCKED 页

正在迁移的页面已从 LRU 列表中隔离,并在取消映射页面、更新页面的地址空间条目以及复制内容和状态的过程中保持锁定,直到页表条目被替换为指向新页面的条目。Linux 支持迁移 mlocked 页面和其他不可驱逐的页面。当旧页面从最后一个 VM_LOCKED VMA 中取消映射时,会清除旧页面中的 PG_mlocked 标志;当新页面在 VM_LOCKED VMA 中替换迁移条目时,会设置该标志。如果页面因 mlocked 而不可驱逐,则 PG_unevictable 标志会跟随 PG_mlocked 标志;但如果页面因其他原因而不可驱逐,则会显式复制 PG_unevictable 标志。

请注意,页面迁移可能与同一页面的 mlocking 或 munlocking 发生竞争。这通常没有问题,因为页面迁移需要取消映射旧页面的所有 PTE(包括 VM_LOCKED 的 munlock),然后在映射新页面(包括 VM_LOCKED 的 mlock)。页表锁提供了足够的同步。

但是,由于 mlock_vma_pages_range() 首先在 VMA 上设置 VM_LOCKED,然后再 mlocking 任何已存在的页面,如果其中一个页面在 mlock_pte_range() 到达它之前被迁移,它将在 mlock_count 中被计算两次。为了防止这种情况,mlock_vma_pages_range() 会临时将 VMA 标记为 VM_IO,以便 mlock_vma_folio() 会跳过它。

为了完成页面迁移,我们会将旧页面和新页面放回 LRU 中。当迁移进程持有的引用计数被释放时,会释放“不需要的”页面 - 成功时的旧页面,失败时的新页面。

压缩 MLOCKED 页面

可以扫描内存映射以查找可压缩区域,默认行为是允许移动不可驱逐的页面。/proc/sys/vm/compact_unevictable_allowed 控制此行为(请参阅 /proc/sys/vm/ 的文档)。压缩工作主要由页面迁移代码处理,并且将应用与迁移 MLOCKED 页面中描述相同的工作流程。

锁定透明巨页

一个透明巨页在 LRU 列表中由单个条目表示。因此,我们只能使整个复合页面不可驱逐,而不能使单个子页面不可驱逐。

如果用户尝试 mlock() 巨页的一部分,并且没有用户 mlock() 整个巨页,我们希望页面的其余部分是可回收的。

我们不能仅在部分 mlock() 时拆分页面,因为 split_huge_page() 可能会失败,并且不希望出现 syscall 的新的间歇性失败模式。

我们通过将 PTE-mlocked 巨页保留在可驱逐的 LRU 列表中来处理此问题:VM_LOCKED VMA 边界上的 PMD 将被拆分为 PTE 表。

这样,vmscan 可以访问该巨页。在内存压力下,该页面将被拆分,属于 VM_LOCKED VMA 的子页面将被移动到不可驱逐的 LRU 中,其余部分可以被回收。

/proc/meminfo 的不可驱逐和 Mlocked 量不包括那些仅由 VM_LOCKED VMA 中的 PTE 映射的透明巨页的部分。

mmap(MAP_LOCKED) 系统调用处理

除了 mlock()、mlock2() 和 mlockall() 系统调用之外,应用程序可以通过向 mmap() 调用提供 MAP_LOCKED 标志来请求锁定内存区域。但是,这里有一个重要而微妙的区别。如果该范围无法被缺页加载(例如,因为 mm_populate 失败),mmap() + mlock() 将失败并返回 ENOMEM,而 mmap(MAP_LOCKED) 不会失败。映射的区域仍将具有锁定区域的属性 - 页面不会被交换出去 - 但仍然可能会发生主要缺页错误来加载内存。

此外,任何 mmap() 调用或 brk() 调用,如果任务先前调用了带有 MCL_FUTURE 标志的 mlockall(),则会使新映射的内存被锁定。在不可驱逐/mlock 更改之前,内核只是调用 make_pages_present() 来分配页面并填充页表。

为了在不可驱逐/mlock 基础结构下锁定内存范围,mmap() 处理程序和任务地址空间扩展函数会调用 populate_vma_page_range(),指定要锁定的 vma 和地址范围。

munmap()/exit()/exec() 系统调用处理

当取消映射 mlocked 内存区域时,无论是通过显式调用 munmap() 还是通过 exit() 或 exec() 处理的内部取消映射,如果我们要删除映射页面的最后一个 VM_LOCKED VMA,我们必须 munlock 这些页面。在不可驱逐/mlock 更改之前,mlocking 没有以任何方式标记页面,因此取消映射它们不需要任何处理。

对于从 VMA 中取消映射的每个 PTE(或 PMD),folio_remove_rmap_*() 会调用 munlock_vma_folio(),当 VMA 为 VM_LOCKED 时,munlock_vma_folio() 会调用 munlock_folio()(除非它是透明巨页的一部分的 PTE 映射)。

munlock_folio() 使用 mlock pagevec 批量处理工作,这些工作将在 lru_lock 下由 __munlock_folio() 完成。__munlock_folio() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 mlocked 标志并清除不可驱逐标志,将页面从不可驱逐状态移动到非活动 LRU。

但是,在实践中,这可能无法理想地工作:页面可能尚未到达“不可驱逐的 LRU”,或者可能已暂时从中隔离。在这些情况下,其 mlock_count 字段是不可用的,必须假定为 0:这样该页面将被拯救到可驱逐的 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,则可能稍后再次被 mlocked。

截断 MLOCKED 页面

文件截断或挖洞强制从用户空间取消映射删除的页面;截断甚至会取消映射和删除任何从现在被截断的文件页面中复制写入的私有匿名页面。

mlocked 页面可以通过这种方式取消锁定和删除:与 munmap() 一样,对于从 VMA 中取消映射的每个 PTE(或 PMD),folio_remove_rmap_*() 会调用 munlock_vma_folio(),当 VMA 为 VM_LOCKED 时,munlock_vma_folio() 会调用 munlock_folio()(除非它是透明巨页的一部分的 PTE 映射)。

但是,如果存在竞争的 munlock(),由于 mlock_vma_pages_range() 首先通过清除 VMA 中的 VM_LOCKED 来开始取消锁定,如果在 mlock_pte_range() 到达它之前,其中一个页面被截断或挖洞取消映射,则该页面不会被此 VMA 识别为 mlocked,并且不会从 mlock_count 中被计数出去。在极少数情况下,一个页面可能在完全取消映射后仍然显示为 PG_mlocked:并且由 release_pages() (或 __page_cache_release())来清除它并在释放之前更新统计信息(此事件在 /proc/vmstat unevictable_pgs_cleared 中计数,通常为 0)。

shrink_*_list() 中的页面回收

vmscan 的 shrink_active_list() 会剔除任何明显的不可驱逐页面,即 !page_evictable(page) 页面,将这些页面转移到不可驱逐列表中。但是,shrink_active_list() 只会看到进入活动/非活动 LRU 列表的不可驱逐页面。请注意,这些页面没有设置 PG_unevictable 标志 - 否则它们将在不可驱逐列表中,并且 shrink_active_list() 永远不会看到它们。

LRU 列表中这些不可驱逐页面的一些示例是

  1. 首次分配时放置在 LRU 列表中的 ramfs 页面。

  2. SHM_LOCK'ed 共享内存页面。shmctl(SHM_LOCK) 不会尝试分配或缺页加载共享内存区域中的页面。当应用程序在 SHM_LOCK'ing 段之后第一次访问该页面时,会发生这种情况。

  3. 仍然映射到 VM_LOCKED VMA 中的页面,这些页面应该被标记为 mlocked,但由于某些事件导致 mlock_count 太低,因此它们被过早地取消锁定。

vmscan 的 shrink_inactive_list() 和 shrink_folio_list() 也会将非活动列表中发现的明显不可驱逐的页面转移到相应的内存 cgroup 和节点不可驱逐列表中。

rmap 的 folio_referenced_one()(通过 vmscan 的 shrink_active_list() 或 shrink_folio_list() 调用)和 rmap 的 try_to_unmap_one()(通过 shrink_folio_list() 调用)会检查(3)仍然映射到 VM_LOCKED VMA 中的页面,并调用 mlock_vma_folio() 来纠正它们。当收缩器释放时,此类页面将被剔除到不可驱逐列表中。