内存管理-76-LRU-1-unevictable-lru-1-unevictable-lru.rst 翻译
一、unevictable-lru.rst
注: 翻译自 kernel-6.1/Documentation/mm/unevictable-lru.rst
==============================
Unevictable LRU Infrastructure
==============================
1. 引言 (Introduction)
本文档描述了 Linux 内存管理器的“不可回收 LRU (Unevictable LRU)”基础设施,以及如何利用它来管理几种类型的“不可回收”页面。
本文档试图提供该机制背后的总体基本原理,以及推动该实现的一些设计决策。后者的设计原理将在实现描述的语境中进行讨论。不可否认,人们可以通过阅读代码来获取实现的细节——即“它做了什么”。但希望下面的描述能够通过回答“它为什么这样做”来提供更多的价值。
2. 不可回收 LRU (The Unevictable LRU)
不可回收 LRU 功能增加了一个额外的 LRU 链表,用于追踪不可回收的页面,并将这些页面对 vmscan(内核的页面回收进程)进行隐藏。该机制基于红帽公司(Red Hat)的 Larry Woodman 提交的一个补丁,旨在解决 Linux 页面回收中的几个扩展性问题。这些问题曾在客户现场的大内存 x86_64 系统上被观察到。
为了用一个例子来说明,一个拥有 128GB 主内存的非 NUMA x86_64 平台在单个节点(Node)中将拥有超过 3200 万个 4k 页面。当这些页面中的很大一部分由于任何原因(见下文)而无法被回收时,vmscan 将花费大量时间扫描 LRU 链表,以寻找那一小部分可以被回收的页面。这可能导致所有 CPU 连续数小时或数天 100% 的时间都耗费在 vmscan 中,从而使系统完全失去响应。
不可回收链表解决了以下几类不可回收页面的问题:
• 由 ramfs 拥有的页面。
• 被映射到经过 SHM_LOCK 锁定的共享内存区域中的页面。
• 被映射到 VM_LOCKED [即通过 mlock() 锁定] 的 VMA 中的页面。
该基础设施在未来可能还有能力处理其他因定义或环境原因而导致页面不可回收的情况。
2.1 不可回收 LRU 页面链表 (The Unevictable LRU Page List)
“不可回收 LRU 页面链表”其实是一个谎言。它从来都不是一个按照 LRU(最近最少使用)顺序排列的链表,而是作为按 LRU 顺序排列的匿名/文件、活跃/非活跃页面链表的伴生链表;而现在,它甚至都不是一个页面链表了。但按照我们熟知的惯例,在此文档和源代码中,我们经常把它想象成第五个 LRU 页面链表。
不可回收 LRU 基础设施包含一个额外的、每个节点(注意是每 node 一套LRU而不是每个 zone 一套, pglist_data::__lruvec, 空闲内存是每 zone 的 zone::free_area)独有的 LRU 链表,称为“不可回收(unevictable)”链表,以及一个相关的页面标志位 PG_unevictable,用于表示该页面正由不可回收链表进行管理。
PG_unevictable 标志位类似于 PG_active 标志位,且与后者互斥。它用于在设置了 PG_lru 标志位时,指示页面驻留在哪一个 LRU 链表上。
不可回收 LRU 基础设施将不可回收的页面维持在如同一个额外的 LRU 链表上,原因有以下几点:
(1) 我们可以“像对待系统中的其他页面一样对待不可回收的页面——这意味着我们可以使用相同的代码来操作它们,使用相同的代码来隔离它们(用于迁移等),使用相同的代码来保持统计数据的追踪等等……”[Rik van Riel]
(2) 我们希望能够在节点之间迁移不可回收的页面,以便进行内存碎片整理、工作负载管理和内存热插拔。Linux 内核只能迁移那些能够成功从 LRU 链表中隔离出来的页面(或者“可移动/Movable”页面,这不在本文讨论范围内)。如果我们把页面维护在 LRU 链表之外的其他地方,导致它们无法被 isolate_lru_page() 检测到,我们就会阻碍它们的迁移。
不可回收链表不会区分文件后备页(file-backed pages)和匿名的、有 swap 后备的页面(anonymous, swap-backed pages)。这种区分只有在页面实际上可以被回收时才重要。
不可回收链表得益于最初由 Christoph Lameter 提出并发布的“数组化(arrayification)”每个节点的 LRU 链表和统计信息的设计。
2.2 与内存控制组的交互 (Memory Control Group Interaction)
不可回收 LRU 功能通过扩展 lru_list 枚举,与内存控制组 [又称内存控制器(memory controller);参见 Documentation/admin-guide/cgroup-v1/memory.rst] 进行交互。
由于每个节点的 LRU 链表实现了“数组化”(每个 lru_list 枚举元素对应一个),内存控制组的数据结构会自动获得一个属于每个节点的不可回收链表。内存控制器会追踪页面移入和移出不可回收链表的动态。
当一个内存控制组面临内存压力时,控制器不会尝试回收不可回收链表上的页面。这会带来以下两个影响:
(1) 因为这些页面在不可回收链表上对回收进程进行了“隐藏”,回收过程可以变得更加高效,只需要处理那些有希望被回收的页面。
(2) 另一方面,如果扣记(charged)到该控制组的页面中有太多是不可回收的,那么该控制组内任务的工作集(working set)中可回收的部分可能就无法装入可用内存中。这会导致控制组发生内存颠簸或触发 OOM-kill 终止任务。
2.3 将地址空间标记为不可回收 (Marking Address Spaces Unevictable)
对于像 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(系统 V 共享内存)使用,用于标记经过 SHM_LOCK 锁定的地址空间,直到调用 SHM_UNLOCK 为止。请注意,如果锁定的页面被换出(swapped out),SHM_LOCK 并不要求将它们换入(page in)内存;如果应用程序想要确保它们留在内存中,必须手动触碰(touch)这些页面。
(3) 由 i915 驱动程序使用,用于标记被固定的地址空间,直到它被取消固定。由 i915 驱动程序标记的不可回收内存量,大致等同于 debugfs/dri/0/i915_gem_objects 中有界的内存对象大小。
2.4 检测不可回收页面 (Detecting Unevictable Pages)
mm/internal.h 中的 page_evictable() 函数利用上面概述的查询函数 [参见章节 :ref:将地址空间标记为不可回收 <mark_addr_space_unevict>] 来检查 AS_UNEVICTABLE 标志位,从而确定一个页面是否可回收。
对于那些在被填充后才进行标记的地址空间(例如 SHM 共享内存区域),锁定操作(例如 SHM_LOCK)可以是惰性(lazy)的,不需要像 mlock() 那样为该区域填充页表,也不需要专门花费精力将 SHM_LOCK 区域内的任何页面推进不可回收链表中。相反,当 vmscan 在回收扫描期间遇到这些页面时,它会顺带完成这个操作。
在执行解锁操作(例如 SHM_UNLOCK)时,解锁者(例如 shmctl())必须扫描该区域中的页面,如果当前没有其他条件限制它们,则需要将它们从不可回收链表中“解救(rescue)”出来。如果一个不可回收的区域被销毁,在释放这些页面的过程中,它们也会被从不可回收链表中“解救”出来。
page_evictable() 还会通过测试一个额外的页面标志位 PG_mlocked [由 PageMlocked() 封装] 来检查被 mlock 锁定的页面。当页面因缺页异常进入 VM_LOCKED 的 VMA,或者在某个 VMA 被设置为 VM_LOCKED 时被发现,该标志位就会被设置。
2.5 Vmscan 对不可回收页面的处理 (Vmscan's Handling of Unevictable Pages)
如果在缺页异常路径(fault path)中淘汰了不可回收页面,或者在调用 mlock() 或 mmap() 时将其移至不可回收链表,vmscan 将不会遇到这些页面,直到它们再次变为可回收(例如通过 munlock())并从不可回收链表中被“拯救”出来。然而,在某些情况下,为了权宜之计,我们可能会决定将不可回收页面留在常规的活跃/非活跃 LRU 链表上,交由 vmscan 处理。vmscan 会在所有 shrink_{active|inactive|page}_list() 函数中检查此类页面,并将其遇到的这类页面进行“淘汰”(cull):也就是说,它会将这些页面重定向到正被扫描的内存控制组(memory cgroup)和内存节点(node)的不可回收链表中。
在某些情况下,某个页面被映射到一个 VM_LOCKED 的虚拟内存区域(VMA),但该页面并未被标记为 PG_mlocked。此类页面会一路到达 shrink_active_list() 或 shrink_page_list(),并在 vmscan 遍历 folio_referenced() 或 try_to_unmap() 中的反向映射(reverse map)时被检测到。当该页面被回收器(shrinker)释放时,它会被淘汰到不可回收链表中。
为了“淘汰”一个不可回收页面,vmscan 在丢弃页面锁(page lock)之后,只需通过 putback_lru_page()(即 isolate_lru_page() 的逆操作)将页面放回 LRU 链表即可。由于使页面变为不可回收的条件在页面解锁后可能会发生变化,__pagevec_lru_add_fn() 在将页面放入不可回收链表之前会重新检查页面的不可回收状态。
3. 加锁页面(MLOCKED Pages)
除了 ramfs 和 SYSV SHM 之外,不可回收页面链表对于 mlock() 也非常有用。需要注意的是,mlock() 仅在 CONFIG_MMU=y 的情况下可用;在无 MMU(NOMMU)的情况下,所有的内存映射实际上都是默认加锁的。
3.1 历史背景
“不可回收加锁页面”(Unevictable mlocked Pages)的基础架构基于 Nick Piggin 最初发布的一份名为 “mm: mlocked pages off LRU” 的 RFC 补丁。Nick 发布这个补丁是为了作为 Christoph Lameter 发布的用于实现相同目标(对 vmscan 隐藏加锁页面)的补丁的替代方案。
在 Nick 的补丁中,他使用 struct page 的 LRU 链表链接字段之一,来作为映射该页面的 VM_LOCKED VMA 的计数(Rik van Riel 在三年前也有过相同的想法)。但是,将链接字段用于计数妨碍了页面在 LRU 链表上的管理,因此加锁页面无法被迁移,因为 isolate_lru_page() 无法检测到它们,且 LRU 链表链接字段对迁移子系统不可用。
Nick 通过在尝试隔离加锁页面之前将其放回 LRU 链表来解决这个问题,从而放弃了对 VM_LOCKED VMA 的计数。当 Nick 的补丁与不可回收 LRU(Unevictable LRU)工作进行整合时,该计数被替换为在执行 munlock 时遍历反向映射,以确定是否仍有其他 VM_LOCKED VMA 映射该页面。
然而,在执行 munlock 时为每个页面遍历反向映射既丑陋又低效,并且当许多对其进行了加锁的进程试图退出时,可能会导致文件 rmap 锁上的灾难性竞争。在 5.18 内核中,在不可回收 LRU 链表链接字段中保持 mlock_count 的想法被重新启用并付诸实践,同时没有妨碍加锁页面的迁移。这就是为什么现在“不可回收 LRU 链表”不能是一个简单的页面双向链表的原因;不过这个链表无论如何也没有其他用途——尽管它的体积大小仍会为了 meminfo 的统计而维护。
3.2 基本管理
加锁页面(即映射到 VM_LOCKED VMA 中的页面)是一类不可回收页面。当此类页面被内存管理子系统“注意到”时,该页面会被打上 PG_mlocked 标志。这可以通过 PageMlocked() 函数进行操作。
PG_mlocked 页面在被添加到 LRU 时,会被放置到不可回收链表上。内存管理可以在几个地方“注意到”此类页面:
(1) 在 mlock()、mlock2()、mlockall() 系统调用处理函数中;
(2) 在使用 MAP_LOCKED 标志进行 mmap 映射区域的 mmap() 系统调用处理函数中;
(3) 在调用了带有 MCL_FUTURE 标志的 mlockall() 的任务中映射区域时;
(4) 在缺页异常路径中,以及扩展 VM_LOCKED 栈段时;或者
(5) 如上所述,在 vmscan:shrink_page_list() 中,当试图通过 folio_referenced() 或 try_to_unmap() 回收 VM_LOCKED VMA 中的页面时。
加锁页面在以下情况下会解除加锁并从不可回收链表中被拯救出来:
(1) 在通过 munlock() / munlockall() 系统调用解锁的地址范围内被映射;
(2) 从映射该页面的最后一个 VM_LOCKED VMA 中被 munmap() 卸载,包括在任务退出时的解除映射;
(3) 当页面被从一个被 mmap 映射的文件的最后一个 VM_LOCKED VMA 中截断(truncate)时;或者
(4) 在 VM_LOCKED VMA 中发生写时复制(COW)之前。
3.3 mlock()/mlock2()/mlockall() 系统调用处理
mlock()、mlock2() 和 mlockall() 系统调用处理函数会针对调用中指定的范围内的每个 VMA 执行 mlock_fixup()。对于 mlockall() 而言,这是任务的整个活动地址空间。注意,mlock_fixup() 既用于对一段内存范围进行加锁,也用于解锁。对一个已经是 VM_LOCKED 的 VMA 调用 mlock(),或者对一个未加锁的 VMA 调用 munlock(),都会被当作空操作处理,mlock_fixup() 会直接返回。
如果 VMA 通过了下文“过滤特殊 VMA”中描述的某些过滤条件,mlock_fixup() 将尝试把该 VMA 与其相邻项合并,或者如果该范围没有覆盖整个 VMA,则分拆出一个 VMA 子集。然后,VMA 中已经存在的任何页面都会通过 mlock_vma_pages_range() -> walk_page_range() -> mlock_pte_range() -> mlock_page() 被标记为加锁。
在从系统调用返回之前,do_mlock() 或 mlockall() 会调用 __mm_populate(),通过 get_user_pages() 调入剩余的页面,并在页面发生缺页时将其标记为加锁。
请注意,正在加锁的 VMA 可能会使用 PROT_NONE 进行映射。在这种情况下,get_user_pages() 将无法调入页面。这没有关系。如果页面最终确实被调入这个 VM_LOCKED VMA 中,它们将在缺页异常路径中得到处理——这也是处理 mlock2() 的 MLOCK_ONFAULT 区域的方式。
对于正被调入 VMA 的每个页表项(PTE 或 PMD),页面添加 rmap 函数会调用 mlock_vma_page(),当 VMA 是 VM_LOCKED 时它会调用 mlock_page()(除非它是透明巨页的一部分的 PTE 映射)。或者,当它是一个新分配的匿名页时,lru_cache_add_inactive_or_unevictable() 会改用 mlock_new_page():这类似于 mlock_page(),但可以做出更好的判断,因为该页面是被独占持有的,且已知尚未在 LRU 上。
mlock_page() 会立即设置 PageMlocked,然后将页面放入 CPU 的 mlock pagevec 中,以便在 lru_lock 下由 __mlock_page() 批量完成其余的工作。__mlock_page() 设置 PageUnevictable,初始化 mlock_count,并将页面移动到不可回收状态(即“不可回收 LRU”,但用 mlock_count 代替了 LRU 链表指针)。或者,如果页面已经是 PageLRU、PageUnevictable 和 PageMlocked,它只需递增 mlock_count。
但在实践中,这可能无法达到理想效果:页面可能尚未处于 LRU 上,或者可能被暂时从 LRU 中隔离了。在这种情况下,mlock_count 字段无法被触及,但稍后当 __pagevec_lru_add_fn() 将页面返回给“LRU”时,它会被设为 0。竞态条件禁止此时将 mlock_count 设为 1:与其冒着让页面无限期作为不可回收页面滞留的风险,不如在 mlock_count 偏低时总是采取保守策略,这样当解锁时,页面将被拯救到一个可回收的 LRU 中,然后如果 vmscan 随后在 VM_LOCKED VMA 中找到它,可能会再次将其加锁。
3.4 过滤特殊 VMA
mlock_fixup() 会过滤几类“特殊”VMA:
(1) 设置了 VM_VM_IO 或 VM_PFNMAP 的 VMA 会被完全跳过。这些映射背后的页面本身就是被固定(pinned)的,因此我们不需要将它们标记为加锁。无论如何,大多数页面没有 struct page 结构来存放这种标记。正因为如此,get_user_pages() 对于这些 VMA 将会失败,因此尝试访问它们是没有意义的。
(2) 映射 hugetlbfs 页面的 VMA 实际上已经被固定在内存中了。我们既不需要也不希望对这些页面执行 mlock()。但是 __mm_populate() 包含了 hugetlbfs 范围,负责分配巨页并填充 PTE。
(3) 设置了 VM_DONTEXPAND 的 VMA 通常是用户空间对内核页面的映射,例如 VDSO 页面、中继通道(relay channel)页面等。这些页面本身就是不可回收的,并且不在 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 中。
3.5 munlock()/munlockall() 系统调用处理
munlock() 和 munlockall() 系统调用与 mlock()、mlock2() 和 mlockall() 系统调用由同一个 mlock_fixup() 函数处理。如果调用它是为了对一个已经解锁的 VMA 进行解锁,mlock_fixup() 会直接返回。由于前面讨论的 VMA 过滤机制,VM_LOCKED 不会设置在任何“特殊”VMA 中。因此,这些 VMA 在解锁时会被忽略。
如果 VMA 是 VM_LOCKED,mlock_fixup() 再次尝试合并或分拆指定的范围。接着,VMA 中的所有页面都会通过 mlock_vma_pages_range() -> walk_page_range() -> mlock_pte_range() -> munlock_page() 被解除加锁——这与加锁 VMA 范围时使用的函数相同,只是带有指示正在执行 munlock() 的新 VMA 标志。
munlock_page() 使用 mlock pagevec 在 lru_lock 下由 __munlock_page() 批量完成工作。__munlock_page() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 PageMlocked 和 PageUnevictable,将页面从不可回收状态移动到非活跃 LRU。
但在实践中,这可能无法达到理想效果:页面可能尚未到达“不可回收 LRU”,或者可能被暂时从其中隔离了。在这种情况下,其 mlock_count 字段不可用,必须假定为 0:这样页面就会被拯救到一个可回收的 LRU 中,然后如果 vmscan 稍后在 VM_LOCKED VMA 中发现它,可能会再次将其加锁。
3.6 迁移加锁页面(Migrating MLOCKED Pages)
正在迁移的页面已被从 LRU 链表中隔离,并在整个页面取消映射、更新页面的地址空间条目、复制内容和状态的过程中保持加锁状态,直到页表条目被替换为指向新页面的条目。Linux 支持加锁页面以及其他不可回收页面的迁移。当旧页面从最后一个 VM_LOCKED VMA 取消映射时,会清除其 PG_mlocked 标志;而在 VM_LOCKED VMA 中映射新页面以替代迁移条目时,会设置该标志。如果页面是因为加锁而不可回收的,PG_unevictable 会跟随 PG_mlocked;但如果页面是因为其他原因不可回收,则会显式复制 PG_unevictable。
请注意,页面迁移可能会与对同一个页面的加锁或解锁操作发生竞态。通常这没有问题,因为页面迁移需要取消映射旧页面的所有 PTE(包括在 VM_LOCKED 时的解锁),然后映射新页面(包括在 VM_LOCKED 时的加锁)。页表锁提供了足够的同步机制。
然而,由于 mlock_vma_pages_range() 首先在 VMA 上设置 VM_LOCKED,在对已经存在的任何页面加锁之前,如果这些页面之一在 mlock_pte_range() 到达之前被迁移了,它就会在 mlock_count 中被计算两次。为了防止这种情况,mlock_vma_pages_range() 临时将 VMA 标记为 VM_IO,以便 mlock_vma_page() 会跳过它。
为了完成页面迁移,我们随后将旧页面和新页面放回 LRU。那个“不需要的”页面(成功时为旧页面,失败时为新页面)会在迁移进程持有的引用计数被释放时被释放。
3.7 压缩加锁页面(Compacting MLOCKED Pages)
可以扫描内存映射以查找可压缩的区域,默认行为是允许移动不可回收页面。/proc/sys/vm/compact_unevictable_allowed 控制此行为(参见 Documentation/admin-guide/sysctl/vm.rst)。压缩工作主要由页面迁移代码处理,并适用与“迁移加锁页面”中所述相同的的工作流程。
3.8 加锁透明巨页(MLOCKING Transparent Huge Pages)
透明巨页(Transparent Huge Page, THP)在 LRU 链表上由单个条目表示。因此,我们只能使整个复合页(compound page)不可回收,而不能使单个子页不可回收。
如果用户尝试对巨页的一部分进行 mlock(),并且没有用户对整个巨页进行 mlock(),我们希望该页面的其余部分是可回收的。
我们不能在局部加锁时简单地拆分页面,因为 split_huge_page() 可能会失败,并且系统调用出现一种新的间歇性失败模式是不受欢迎的。
我们通过将 PTE 加锁的巨页保留在可回收 LRU 链表上来处理这个问题:VM_LOCKED VMA 边界上的 PMD 将被拆分为一个 PTE 表。
这样一来,巨页对于 vmscan 来说就是可访问的。在内存压力下,页面将被拆分,属于 VM_LOCKED VMA 的子页将被移动到不可回收 LRU 中,其余部分则可以被回收。
/proc/meminfo 中的不可回收(Unevictable)和加锁(Mlocked)统计量不包括透明巨页中那些仅由 VM_LOCKED VMA 中的 PTE 映射的部分。
3.9 mmap(MAP_LOCKED) 系统调用处理
除了 mlock()、mlock2() 和 mlockall() 系统调用之外,应用程序还可以通过向 mmap() 调用提供 MAP_LOCKED 标志,来请求对一段内存区域进行加锁。不过,这里有一个重要且微妙的区别。mmap() + mlock() 如果无法调入内存范围(例如因为 mm_populate 失败)将会失败并返回 ENOMEM,而 mmap(MAP_LOCKED) 则不会失败。mmap 映射的区域仍将具备加锁区域的属性——页面不会被换出——但为了调入内存而引发的主缺页异常可能仍然会发生。
此外,任何由先前曾调用过带 MCL_FUTURE 标志的 mlockall() 的任务所发起的、扩展堆的 mmap() 或 brk() 调用,都会导致新映射的内存被加锁。在不可回收/加锁修改之前,内核只是简单地调用 make_pages_present() 来分配页面并填充页表。
为了在不可回收/加锁基础架构下对一段内存范围加锁,mmap 处理函数和任务地址空间扩展函数会调用 populate_vma_page_range(),并指定要加锁的 vma 和地址范围。
3.10 munmap()/exit()/exec() 系统调用处理
当解除映射一段加锁的内存区域时(无论是通过显式调用 munmap(),还是通过 exit() 或 exec() 处理过程中的内部解除映射),如果我们正在移除映射这些页面的最后一个 VM_LOCKED VMA,我们就必须对这些页面进行解锁。在不可回收/加锁修改之前,加锁不会以任何方式标记页面,因此解除映射它们不需要任何处理。
对于正从 VMA 中解除映射的每个 PTE(或 PMD),page_remove_rmap() 会调用 munlock_vma_page(),当 VMA 是 VM_LOCKED 时它会调用 munlock_page()(除非它是透明巨页的一部分的 PTE 映射)。
munlock_page() 使用 mlock pagevec 在 lru_lock 下由 __munlock_page() 批量完成工作。__munlock_page() 递减页面的 mlock_count,当该计数达到 0 时,它会清除 PageMlocked 和 PageUnevictable,将页面从不可回收状态移动到非活跃 LRU。
但在实践中,这可能无法达到理想效果:页面可能尚未到达“不可回收 LRU”,或者可能被暂时从其中隔离了。在这种情况下,其 mlock_count 字段不可用,必须假定为 0:这样页面就会被拯救到一个可回收的 LRU 中,然后如果 vmscan 稍后在 VM_LOCKED VMA 中发现它,可能会再次将其加锁。
3.11 截断加锁页面(Truncating MLOCKED Pages)
文件截断(truncation)或孔洞穿刺(hole punching)会强制从用户空间解除映射被删除的页面;截断甚至会解除映射并删除任何从当前被截断的文件页进行过写时复制(COW)的私有匿名页。
加锁页面可以通过这种方式解锁并删除:与 munmap() 一样,对于正从 VMA 中解除映射的每个 PTE(或 PMD),page_remove_rmap() 会调用 munlock_vma_page(),当 VMA 是 VM_LOCKED 时它会调用 munlock_page()(除非它是透明巨页的一部分的 PTE 映射)。
然而,如果存在与 munlock() 的竞态,由于 mlock_vma_pages_range() 通过从 VMA 清除 VM_LOCKED 来开始解锁,在解锁所有现有页面之前,如果其中一个页面在 mlock_pte_range() 到达之前被截断或孔洞穿刺解除映射了,它就不会被此 VMA 识别为加锁页面,也不会从 mlock_count 中扣除。在这种罕见情况下,页面在被完全解除映射后可能仍显示为 PageMlocked:这时留给 release_pages()(或 __page_cache_release())来清除它并在释放前更新统计信息(此事件在 /proc/vmstat 的 unevictable_pgs_cleared 中计数,通常为 0)。
3.12 在 shrink_*_list() 中回收页面
vmscan 的 shrink_active_list() 会淘汰任何明显的不可回收页面——即 !page_evictable(page) 的页面——将其重定向到不可回收链表。但是,shrink_active_list() 只能看到进入了活跃/非活跃 LRU 链表的不可回收页面。注意,这些页面没有设置 PageUnevictable 标志——否则它们就会在不可回收链表上,shrink_active_list() 永远不会看到它们。
LRU 链表上这些不可回收页面的某些例子包括:
(1) 在首次分配时被放置到 LRU 链表上的 ramfs 页面。
(2) 经过 SHM_LOCK 加锁的共享内存页。shmctl(SHM_LOCK) 不会试图在共享内存区域中分配页面或引发缺页异常。这发生在应用程序在对该段进行 SHM_LOCK 加锁后第一次访问该页面时。
(3) 仍然映射到 VM_LOCKED VMA 中的页面,它们本应被标记为加锁,但事件导致 mlock_count 过低,因此它们被过早地解锁了。
vmscan 的 shrink_inactive_list() 和 shrink_page_list() 也会将非活跃链表上发现的明显不可回收页面重定向到适当的内存控制组和节点的不可回收链表中。
通过 vmscan 的 shrink_active_list() 或 shrink_page_list() 调用的 rmap 的 folio_referenced_one(),以及通过 shrink_page_list() 调用的 rmap 的 try_to_unmap_one(),会检查上述第 (3) 类仍然映射到 VM_LOCKED VMA 中的页面,并调用 mlock_vma_page() 来纠正它们。此类页面在被回收器释放时会被淘汰到不可回收链表中。
posted on 2026-10-04 14:42 Hello-World3 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号