不可驱逐 LRU 基础设施¶
简介¶
本文档介绍了 Linux 内存管理器的“不可驱逐 LRU”基础设施,以及使用该基础设施管理几种类型的“不可驱逐”页。
本文档试图提供该机制的总体原理,以及一些驱动实现的背后设计决策的原理。后面的设计原理将在实现描述的上下文中讨论。诚然,人们可以通过阅读代码获得实现细节——“它做什么?”。我们希望下面的描述通过提供“它为什么要这样做?”的答案来增加价值。
不可驱逐 LRU¶
不可驱逐 LRU 设施添加了一个额外的 LRU 列表来跟踪不可驱逐的页面,并将这些页面从 vmscan 中隐藏起来。这种机制基于 Red Hat 的 Larry Woodman 的一个补丁,该补丁旨在解决 Linux 中页面回收的几个可伸缩性问题。这些问题已在客户站点的大内存 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 页清单¶
不可驱逐 LRU 页清单是一个谎言。它从来不是一个按 LRU 排序的列表,而是按 LRU 排序的匿名和文件、活动和非活动页清单的伴侣;现在它甚至不是一个页清单。但按照熟悉的惯例,在此文档和源代码中,我们经常将其想象为第五个 LRU 页清单。
不可驱逐 LRU 基础设施由一个额外的、每个节点的 LRU 列表组成,称为“不可驱逐”列表,以及一个相关的页标志 PG_unevictable,用于指示该页正在不可驱逐列表上进行管理。
PG_unevictable 标志类似于 PG_active 标志,并且与其互斥,因为它指示设置 PG_lru 时页驻留在哪个 LRU 列表上。
不可驱逐 LRU 基础设施维护不可驱逐页,就好像它们在额外的 LRU 列表上,原因如下
我们可以“像对待系统中的其他页一样对待不可驱逐页——这意味着我们可以使用相同的代码来操作它们,相同的代码来隔离它们(用于迁移等),相同的代码来跟踪统计信息,等等……”[Rik van Riel]
我们希望能够在节点之间迁移不可驱逐页,以进行内存碎片整理、工作负载管理和内存热插拔。Linux 内核只能迁移它可以成功地从 LRU 列表隔离出来的页(或“可移动”页:此处不予考虑)。如果我们将页维护在 LRU 类列表之外的其他地方,
folio_isolate_lru()
可以检测到它们,我们将阻止它们的迁移。
不可驱逐列表不区分文件支持的页和匿名、交换支持的页。这种区分只有在页实际上可以驱逐时才重要。
不可驱逐列表受益于 Christoph Lameter 最初提出并发布的每个节点 LRU 列表和统计信息的“数组化”。
内存控制组交互¶
不可驱逐 LRU 设施通过扩展 lru_list 枚举来与内存控制组[又名内存控制器;参见 内存资源控制器]交互。
由于每个节点 LRU 列表的“数组化”(每个 lru_list 枚举元素一个),内存控制器数据结构自动获得每个节点的不可驱逐列表。内存控制器跟踪页面进出不可驱逐列表的移动。
当内存控制组承受内存压力时,控制器不会尝试回收不可驱逐列表上的页面。这会产生几个影响
由于页面在不可驱逐列表中被“隐藏”起来无法回收,因此回收过程可能更有效,仅处理有可能被回收的页面。
另一方面,如果分配给控制组的页面太多是不可驱逐的,则控制组中任务工作集的可以驱逐部分可能无法放入可用内存中。这可能导致控制组抖动或 OOM-kill 任务。
标记地址空间为不可驱逐¶
对于 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。
这些目前在内核中的三个地方使用
ramfs 在创建其 inode 时标记其 inode 的地址空间,并且此标记在 inode 的生命周期内保持不变。
SYSV SHM 标记 SHM_LOCK'd 地址空间,直到调用 SHM_UNLOCK。请注意,如果锁定的页面已交换出去,则不需要 SHM_LOCK 将其页面调入;如果应用程序要确保它们在内存中,则必须手动触摸这些页面。
i915 驱动程序标记已固定的地址空间,直到它被取消固定。i915 驱动程序标记的不可驱逐内存量大致是 debugfs/dri/0/i915_gem_objects 中的有界对象大小。
检测不可驱逐页¶
mm/internal.h 中的函数 folio_evictable() 使用上面概述的查询函数[参见 标记地址空间为不可驱逐]来检查 AS_UNEVICTABLE 标志,以确定页面是否可以驱逐。
对于在填充后被如此标记的地址空间(如 SHM 区域),锁定操作(例如 SHM_LOCK)可以是懒惰的,不需要像 mlock() 那样为该区域填充页表,也不需要特别努力将 SHM_LOCK'd 区域中的任何页面推送到不可驱逐列表。相反,如果 vmscan 在回收扫描期间遇到页面,它将这样做。
在解锁操作(如 SHM_UNLOCK)中,解锁器(例如 shmctl())必须扫描该区域中的页面,并从不可驱逐列表中“拯救”它们,如果没有其他条件使它们保持不可驱逐状态。如果不可驱逐区域被销毁,页面也会在释放它们的过程中从不可驱逐列表中“拯救”出来。
folio_evictable() 还会通过调用 folio_test_mlocked() 来检查 mlocked 页面,该函数在页面被错误地放入 VM_LOCKED VMA 中,或者在 VM_LOCKED 的 VMA 中找到时设置。
Vmscan 对不可驱逐页的处理¶
如果在错误路径中剔除不可驱逐页,或者在 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()
中遍历反向映射时会检测到它们。当收缩器释放页面时,该页面将被剔除到不可驱逐列表。
要“剔除”不可驱逐页面,vmscan 只需使用 folio_putback_lru()
将页面放回 LRU 列表中,这是 folio_isolate_lru()
的逆操作,在删除页面锁后。由于使页面不可驱逐的条件可能会在页面解锁后发生变化,因此 __pagevec_lru_add_fn() 将在将页面放置在不可驱逐列表上之前重新检查页面的不可驱逐状态。
MLOCKED 页¶
不可驱逐页面列表对于 mlock() 也很有用,除了 ramfs 和 SYSV SHM。请注意,mlock() 仅在 CONFIG_MMU=y 的情况下可用;在 NOMMU 的情况下,所有映射实际上都是 mlocked 的。
历史¶
“不可驱逐 mlocked 页面”基础设施基于 Nick Piggin 最初在题为“mm: mlocked pages off LRU”的 RFC 补丁中发布的工作。Nick 发布他的补丁是为了替代 Christoph Lameter 发布的旨在实现相同目标的补丁:从 vmscan 中隐藏 mlocked 页面。
在 Nick 的补丁中,他使用 struct page LRU 列表链接字段之一作为映射页面的 VM_LOCKED VMA 的计数(Rik van Riel 在三年前也有同样的想法)。但是,将链接字段用于计数会阻止在 LRU 列表上管理页面,因此 mlocked 页面无法迁移,因为 folio_isolate_lru()
无法检测到它们,并且 LRU 列表链接字段对迁移子系统不可用。
Nick 通过在尝试隔离 mlocked 页面之前将其放回 LRU 列表上来解决此问题,从而放弃了 VM_LOCKED VMA 的计数。当 Nick 的补丁与不可驱逐 LRU 工作集成时,该计数被在解锁时遍历反向映射以确定是否有任何其他 VM_LOCKED VMA 仍然映射页面的过程所取代。
但是,在 munlocking 时遍历每个页面的反向映射既丑陋又效率低下,并且当许多 mlocked 进程试图退出时,可能会导致文件 rmap 锁上的灾难性争用。在 5.18 中,重新启用并将 mlock_count 保存在不可驱逐 LRU 列表链接字段中的想法付诸实践,而不会阻止 mlocked 页面的迁移。这就是为什么“不可驱逐 LRU 列表”现在不能是页面链表的原因;但是无论如何都没有使用链表——尽管它的尺寸是为了 meminfo 而维护的。
基本管理¶
mlocked 页面——映射到 VM_LOCKED VMA 中的页面——是一类不可驱逐页面。当内存管理子系统“注意到”此类页面时,该页会被标记为 PG_mlocked 标志。可以使用 folio_set_mlocked() 和 folio_clear_mlocked() 函数来操作此标志。
当 PG_mlocked 页面添加到 LRU 时,它将放置在不可驱逐列表上。内存管理可以在以下几个位置“注意到”此类页面
在 mlock()/mlock2()/mlockall() 系统调用处理程序中;
在使用 MAP_LOCKED 标志 mmapping 区域时,在 mmap() 系统调用处理程序中;
在使用 MCL_FUTURE 标志调用 mlockall() 的任务中映射一个区域;
在错误路径中以及扩展 VM_LOCKED 堆栈段时;或
如上所述,在 vmscan:shrink_folio_list() 中,当尝试通过
folio_referenced()
或try_to_unmap()
回收 VM_LOCKED VMA 中的页面时。
在以下情况下,mlocked 页面将变为已解锁并从不可驱逐列表中拯救出来
在通过 munlock()/munlockall() 系统调用解锁的范围内映射;
从映射页面的最后一个 VM_LOCKED VMA 中 munmap(),包括在任务退出时取消映射;
当页面从 mmapped 文件的最后一个 VM_LOCKED VMA 中截断时;或
在 VM_LOCKED VMA 中页面被 COW'd 之前。
mlock()/mlock2()/mlockall() 系统调用处理¶
mlock()、mlock2() 和 mlockall() 系统调用处理程序继续对调用指定的范围内的每个 VMA 执行 mlock_fixup()。在 mlockall() 的情况下,这是任务的整个活动地址空间。请注意,mlock_fixup() 用于锁定和解锁内存范围。对已经 VM_LOCKED 的 VMA 调用 mlock(),或者对未 VM_LOCKED 的 VMA 调用 munlock(),都被视为空操作,mlock_fixup() 只需返回。
如果 VMA 通过了下面的“过滤特殊 VMA”中描述的一些过滤,mlock_fixup() 将尝试将 VMA 与其邻居合并,或者如果范围未覆盖整个 VMA,则拆分 VMA 的子集。然后,VMA 中已经存在的任何页面都会通过 walk_page_range() 通过 mlock_pte_range() 通过 mlock_vma_pages_range() 由 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 页面批处理中,以批处理在 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 错误,以便在解锁时,页面将被拯救到可驱逐 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,可能会在稍后再次锁定。
过滤特殊 VMA¶
mlock_fixup() 过滤了几类“特殊”VMA
设置了 VM_IO 或 VM_PFNMAP 的 VMA 将完全跳过。这些映射后面的页面本质上是被固定的,因此我们不需要将它们标记为 mlocked。在任何情况下,大多数页面都没有 struct page 来标记页面。因此,get_user_pages() 对于这些 VMA 将会失败,因此尝试访问它们是没有意义的。
映射 hugetlbfs 页面的 VMA 已经有效地固定到内存中。我们既不需要也不想 mlock() 这些页面。但是 __mm_populate() 包括 hugetlbfs 范围,分配大页面并填充 PTE。
具有 VM_DONTEXPAND 的 VMA 通常是内核页面的用户空间映射,例如 VDSO 页面、中继通道页面等。这些页面本质上是不可驱逐的,并且不在 LRU 列表上进行管理。__mm_populate() 包括这些范围,填充 PTE(如果尚未填充)。
设置了 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 已经解锁的 VMA,mlock_fixup() 只需返回。由于上面讨论的 VMA 过滤,VM_LOCKED 不会在任何“特殊”VMA 中设置。因此,这些 VMA 将被 munlock 忽略。
如果 VMA 是 VM_LOCKED,mlock_fixup() 将再次尝试合并或拆分指定的范围。然后,通过 walk_page_range() 通过 mlock_pte_range() 通过 mlock_vma_pages_range() 由 munlock_folio() 解锁 VMA 中的所有页面——与 mlocking VMA 范围时使用的函数相同,VMA 的新标志指示正在执行 munlock()。
munlock_folio() 使用 mlock pagevec 来批处理要由 __munlock_folio() 在 lru_lock 下完成的工作。__munlock_folio() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 mlocked 标志并清除不可驱逐标志,从而将页面从不可驱逐状态移动到非活动 LRU。
但在实践中,这可能无法理想地工作:页面可能尚未到达“不可驱逐 LRU”,或者它可能已暂时与 LRU 隔离。在这些情况下,其 mlock_count 字段不可用,必须假定为 0:以便页面将被拯救到可驱逐 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,可能会在稍后再次锁定。
迁移 MLOCKED 页¶
正在迁移的页面已从 LRU 列表中隔离出来,并且在页面的取消映射、更新页面的地址空间条目以及复制内容和状态的过程中保持锁定,直到页表条目被引用新页面的条目替换。Linux 支持 mlocked 页面和其他不可驱逐页面的迁移。当旧页面从最后一个 VM_LOCKED VMA 中取消映射时,PG_mlocked 会从旧页面中清除,并在新页面映射到位以代替 VM_LOCKED VMA 中的迁移条目时设置。如果页面由于 mlocked 而不可驱逐,则 PG_unevictable 遵循 PG_mlocked;但如果页面由于其他原因而不可驱逐,则 PG_unevictable 会被显式复制。
请注意,页面迁移可能会与同一页面的锁定或解锁竞争。这主要没有问题,因为页面迁移需要取消映射旧页面的所有 PTE(包括 VM_LOCKED 的 munlock),然后映射新页面(包括 VM_LOCKED 的 mlock)。页表锁提供了足够的同步。
但是,由于 mlock_vma_pages_range() 首先在 VMA 上设置 VM_LOCKED,然后在锁定任何已存在的页面之前,如果在 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 页面中描述的工作流程相同。
MLOCKING 透明大页¶
透明大页由 LRU 列表上的单个条目表示。因此,我们只能使整个复合页面不可驱逐,而不是单个子页面。
如果用户尝试 mlock() 大页的一部分,并且没有用户 mlock() 整个大页,我们希望页面的其余部分是可回收的。
我们不能像 split_huge_page() 那样在部分 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 标志来请求 mlocked 内存区域。但是,这里有一个重要而微妙的区别。如果范围无法被错误地调入(例如,因为 mm_populate 失败),mmap() + mlock() 将失败并返回 ENOMEM,而 mmap(MAP_LOCKED) 不会失败。mmap 区域仍将具有锁定区域的属性——页面不会被交换出去——但是可能仍然会发生重大页面错误,以将内存错误地调入。
此外,任何 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,我们必须解锁页面。在不可驱逐/mlock 更改之前,mlocking 没有以任何方式标记页面,因此取消映射它们不需要任何处理。
对于从 VMA 中取消映射的每个 PTE(或 PMD),folio_remove_rmap_*() 调用 munlock_vma_folio(),当 VMA 为 VM_LOCKED 时(除非它是透明大页的一部分的 PTE 映射),该函数调用 munlock_folio()。
munlock_folio() 使用 mlock pagevec 来批处理要由 __munlock_folio() 在 lru_lock 下完成的工作。__munlock_folio() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 mlocked 标志并清除不可驱逐标志,从而将页面从不可驱逐状态移动到非活动 LRU。
但在实践中,这可能无法理想地工作:页面可能尚未到达“不可驱逐 LRU”,或者它可能已暂时与 LRU 隔离。在这些情况下,其 mlock_count 字段不可用,必须假定为 0:以便页面将被拯救到可驱逐 LRU,然后如果 vmscan 在 VM_LOCKED VMA 中找到它,可能会在稍后再次锁定。
截断 MLOCKED 页¶
文件截断或空洞强制取消映射用户空间中已删除的页面;截断甚至会取消映射和删除从现在被截断的文件页面复制而来任何私有匿名页面。
可以以这种方式解锁和删除 Mlocked 页面:与 munmap() 类似,对于从 VMA 中取消映射的每个 PTE(或 PMD),folio_remove_rmap_*() 调用 munlock_vma_folio(),当 VMA 为 VM_LOCKED 时(除非它是透明大页的一部分的 PTE 映射),该函数调用 munlock_folio()。
但是,如果存在竞争 munlock(),因为 mlock_vma_pages_range() 首先通过从 VMA 中清除 VM_LOCKED 来开始 munlocking,然后在解锁所有存在的页面之前,如果其中一个页面在 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 列表上这些不可驱逐页面的一些示例包括
首次分配时放置在 LRU 列表上的 ramfs 页面。
SHM_LOCK'd 共享内存页面。shmctl(SHM_LOCK) 不会尝试在共享内存区域中分配或错误调入页面。这会在应用程序在 SHM_LOCK'ing 段之后第一次访问页面时发生。
仍然映射到 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() 调用)检查仍然映射到 VM_LOCKED VMA 中的 (3) 页面,并调用 mlock_vma_folio() 来更正它们。此类页面在收缩器释放后会被剔除到不可驱逐列表。