内存管理-75-MGLRU-2-内核文档翻译-multigen_lru.rst
基于 Linux-6.1
一、mm/multigen_lru.rst
注: 翻译自 /kernel-6.1/Documentation/mm/multigen_lru.rst
=============
多代 LRU(Multi-Gen LRU)
=============
多代 LRU 是一种替代性的 LRU 实现,用于优化页面回收并提升内存压力下的性能。页面回收决定了内核的缓存策略以及内存超配(overcommit)能力,它直接影响kswapd 的 CPU 占用率和内存利用效率。
1. 设计概览
1.1 设计目标
设计目标如下:
* 良好地刻画访问的最近性(access recency)
* 尽力利用空间局部性
* 提供快速路径以便快速做出显而易见的决策
* 简单的可自校正启发式
对访问最近性的刻画是所有 LRU 实现的核心。在多代 LRU 中,每一代(generation)代表一组访问最近性相近的页面。代建立了一个(基于时间的)公共参考系,从而有助于做出更好的决策,例如在同一台机器上不同的 memcg 之间,或数据中心中不同的机器之间(用于作业调度)。
利用空间局部性可以在收集 accessed 位时提高效率。rmap 遍历针对单个页面,无法利用“发现了一个 young PTE”这一收益;而页表遍历可以扫过整个地址空间中所有 young 的 PTE,但地址空间可能过于稀疏,导致收益有限。关键在于两种方法都要优化,并将两者结合使用。
快速路径可以降低代码复杂度和运行时开销。未映射的页面不需要 TLB flush;干净的页面不需要回写。但只有在其他条件(如访问最近性)相近时,这些事实才有意义。有了代作为公共参考系,额外的因素就能凸显出来。然而显而易见的决策未必是好的决策,因此自校正必不可少。
简单可自校正启发式的好处不言自明。同样,有了代作为公共参考系,这一点变得可以实现。具体来说,同一代内的页面可以根据额外因素分类,反馈回路可以统计比较各类之间的 refault 比例,并推断哪一类是更好的选择。
1.2 基本假设
对热页面的保护和对冷页面的选择,基于页面的访问通道(access channel)和访问模式。存在两条访问通道:
* 通过页表的访问
* 通过文件描述符的访问
对前一条通道的保护在设计上更强,原因如下:
1. 前一条通道的访问模式判定不确定性更高,因为 accessed 位只是近似反映访问情况。
2. 驱逐前一条通道的成本更高,因为需要 TLB flush,而且更可能遇到脏位。
3. 对前一条通道保护不足的代价更高,因为应用通常不会像对待阻塞 I/O 那样为 major page fault 做准备。例如 GUI 应用通常使用专门的 I/O 线程, 以避免阻塞渲染线程。
访问模式也有两种:
* 表现出时间局部性的访问
* 未表现出时间局部性的访问
基于上述原因,除非存在 ``VM_SEQ_READ`` 或 ``VM_RAND_READ``,前一条通道被假定为遵循前一种模式;除非观察到异常 refault,后一条通道被假定为遵循后一种模式。
2. 工作流程概览
可驱逐页面在每个 ``lruvec`` 中划分为多个代。最年轻一代的编号同时存于 anon 和 file 两种类型的 ``lrugen->max_seq`` 中,因为两种类型在老化时享有同等地位。最老一代的编号则分别存于两种类型的 ``lrugen->min_seq[]`` 中,因为干净的文件页面无论交换空间是否受限都可以被驱逐。这三个变量都是单调递增的。
为了存入 ``folio->flags`` 中的 gen 计数器,代编号被截断为 ``order_base_2(MAX_NR_GENS+1)`` 位。每个截断后的代编号都是 ``lrugen->folios[]`` 的索引。实现中使用滑动窗口技术来跟踪至少 ``MIN_NR_GENS``、至多 ``MAX_NR_GENS`` 个代。页面在 ``lrugen->folios[]`` 上时,gen 计数器保存 ``[1, MAX_NR_GENS]`` 范围内的值;否则保存零。
每一代又划分为多个层级(tier)。一个页面若通过文件描述符被访问了 ``N`` 次,就位于第 ``order_base_2(N)`` 层。与代不同,层级没有专门的 ``lrugen->folios[]``。跨代移动需要持有 LRU 锁,而跨层移动只对 ``folio->flags`` 做原子操作,因此开销可以忽略。反馈回路仿照 PID 控制器,监视来自 anon 和 file 两种类型所有层级的 refault,并决定驱逐哪些类型中的哪些层级、保护哪些类型中的哪些层级。
存在两个概念上相互独立的过程:老化(aging)和驱逐(eviction)。它们构成一个闭环系统,即页面回收。
2.1 老化
老化产生年轻的代。对一个 ``lruvec`` 而言,当 ``max_seq-min_seq+1`` 逼近 ``MIN_NR_GENS`` 时,老化会对 ``max_seq`` 加一。老化通过页表发现页面被访问时,
会将热页面提升到最年轻一代;而对 ``max_seq`` 的加一本身就会顺带导致冷页面被降级。老化通过页表遍历和 rmap 遍历来发现 young 的 PTE。对于前者,它遍历 ``lruvec_memcg()->mm_list``,用列表上的每个 ``mm_struct`` 调用``walk_page_range()`` 扫描 PTE,每轮遍历结束后将 ``max_seq`` 加一。对于后者,当驱逐遍历 rmap 并发现 young PTE 时,老化会顺带扫描相邻的 PTE。两者在发现 young PTE 时,都会清除 accessed 位,并把映射该 PTE 的页面的 gen 计数器更新为 ``(max_seq%MAX_NR_GENS)+1``。
2.2 驱逐
驱逐消耗年老的代。对一个 ``lruvec`` 而言,当由 ``min_seq%MAX_NR_GENS``索引的 ``lrugen->folios[]`` 变空时,驱逐会对 ``min_seq`` 加一。为了选择驱逐的类型和层级,它先比较 ``min_seq[]`` 选出更老的那个类型;如果两种类型同样老,则选择第一层级 refault 比例较低的那个。第一层级包含“单次使用的、未映射的干净页面”,它们是回收的最佳选择。如果老化发现某页面被页表访问过并更新了它的 gen 计数器,驱逐会根据 gen 计数器对该页面排序。若某页面被文件描述符多次访问、且反馈回路检测到该页面所在层级存在异常 refault,驱逐还会把它移到下一代,即 ``min_seq+1``。为此,反馈回路以第一层级作为基准,原因如前所述。
2.3 工作集保护
每一代在创建时打上时间戳。若设置了 ``lru_gen_min_ttl``,当某个 ``lruvec`` 最老一代的创建时间在 ``lru_gen_min_ttl`` 毫秒以内时,该 ``lruvec`` 将受到
保护不被驱逐。换言之,它可以防止 ``lru_gen_min_ttl`` 毫秒以内的工作集被驱逐。如果该工作集无法留在内存中,则会触发 OOM killer。
这种基于时间的方法有如下优点:
(1) 配置更容易,因为它与应用类型和内存大小无关。
(2) 更可靠,因为它直接挂钩到 OOM killer。
2.4 Rmap/PT 遍历反馈
为了在 LRU 链表中逐页搜索映射它的 PTE(以测试并清除 accessed 位)而遍历 rmap,开销可能很大:来自不同 VMA(PA 空间)的页面对 rmap(VA 空间)而言缓存不友好。对于以映射页面为主的工作负载,rmap 搜索可能占回收路径 CPU 开销的最大头。
``lru_gen_look_around()`` 利用空间局部性减少进入 rmap 的次数。它扫描 young PTE 的相邻 PTE 并提升热页面。如果这次扫描对 cache line 是高效的,它就把指向该 PTE 表的 PMD 表项加入 Bloom 过滤器。这在驱逐与老化之间形成了一条反馈回路。
2.5 Bloom 过滤器
Bloom 过滤器是一种节省空间和内存的集合成员判定数据结构,即判定元素“不在集合中”或“可能在集合中”。
在驱逐路径中,具体是在 ``lru_gen_look_around()`` 里:如果一个 PMD 含有足够多的热页面,就把它的地址放入过滤器。在老化路径中,成员判定为真意味着
会去扫描对应的 PTE 区间,以发现 young 页面。
注意,Bloom 过滤器对集合成员的判定是概率性的。若一次判定为假阳性,代价只是额外扫描一段 PTE 区间,而这本来也可能发现热页面。过滤器自身的
参数可以(在极限意义上)控制假阳性率。
2.6 Memcg LRU
memcg LRU 是按 NUMA 节点的 memcg 链表,也是“LRU 的 LRU”——每个节点与 memcg 的组合各自拥有一条 folio 的 LRU(参见 ``mem_cgroup_lruvec()``)。它的
目标是提高全局回收的可扩展性,这对数据中心中系统级的内存超配至关重要。注意 memcg LRU 只作用于全局回收。
可以把 memcg LRU 的基本结构类比于(folio 的)active/inactive LRU 来理解:
(1) 它有年轻代和年老代,分别对应 active 和 inactive;
(2) ``max_seq`` 加一会触发提升(promotion),对应激活(activation);
(3) 其他事件触发类似操作,例如离线(offlining)一个 memcg 会触发降级,对应去激活(deactivation)。
就全局回收而言,它有两个显著特性:
(1) 分片(sharding):每个线程可以从随机一个(年老代中的)memcg 开始,提高并行度;
(2) 最终公平(eventual fairness):direct reclaim 可以随时中止,在不损害一段时间内公平性的前提下降低延迟。
就全局回收中遍历 memcg 而言,它把最优情况下的复杂度从 O(n) 降为 O(1),且不影响最坏情况复杂度 O(n)。因此平均而言其复杂度是亚线性的(sublinear)。
2.7 小结
(folio 的)多代 LRU 可以拆解为如下部分:
* 代(Generations)
* Rmap 遍历
* 页表遍历
* Bloom 过滤器
* PID 控制器
老化与驱逐构成生产者-消费者模型;具体地说,后者通过代上的滑动窗口驱动前者。在老化内部,rmap 遍历通过把热点密集的页表插入 Bloom 过滤器来驱动
页表遍历。在驱逐内部,PID 控制器以 refault 作为反馈,来选择要驱逐的类型和要保护的层级。
二、guide/multigen_lru.rst
注: 翻译自 /kernel-6.1/Documentation/admin-guide/mm/multigen_lru.rst
=============
多代 LRU (Multi-Gen LRU)
=============
多代 LRU (Multi-Gen LRU) 是一种替代的 LRU 实现方案,可在内存压力下优化页面回收并提升系统性能。页面回收决定了内核的缓存策略以及内存超发(overcommit)的能力,它会直接影响 kswapd 的 CPU 使用率和内存(RAM)的利用效率。
1. 快速入门
在编译内核时启用以下配置:
• CONFIG_LRU_GEN=y
• CONFIG_LRU_GEN_ENABLED=y
配置完成!
2. 运行时选项
/sys/kernel/mm/lru_gen/ 目录下包含了以下各小节中描述的稳定 ABI。
2.1 一键开关 (Kill switch)
enabled 文件接受不同的值,用于启用或禁用以下组件。其默认值取决于 CONFIG_LRU_GEN_ENABLED 的配置。除非某些组件带来了不可预见的副作用,否则应当启用所有组件。当硬件不支持某个组件时,向 enabled 写入内容不会产生任何效果;即使主开关处于关闭状态,合法的输入值依然会被接受。
0x0001: 多代 LRU 的主开关。
0x0002: 当 MMU 设置了访问位时(例如在 x86 架构上),以大批量的形式清除叶内核页表项(leaf page table entries)中的访问位(accessed bit)。这种行为从理论上讲可能会加剧锁竞争(mmap_lock)。如果禁用此功能,对于连续映射热页(hot pages)的业务负载,多代 LRU 的性能会出现轻微下降,因为原本这些热页的访问位可以通过更少、更大批量的操作来统一清除。
0x0004: 当 MMU 设置了访问位时(例如在 x86 架构上),同时清除非叶页表项(non-leaf page table entries)中的访问位。该行为在除 Intel 和 AMD 之外的其他 x86 变体上尚未经过验证。如果禁用此功能,多代 LRU 的性能降幅微乎其微。
[yYnN] 应用于上述所有组件(y/Y 表示全部启用,n/N 表示全部禁用)。
例如:
echo y >/sys/kernel/mm/lru_gen/enabled cat /sys/kernel/mm/lru_gen/enabled 0x0007 echo 5 >/sys/kernel/mm/lru_gen/enabled cat /sys/kernel/mm/lru_gen/enabled 0x0005
注: 实测,最大值是 3,即使 echo 7 也是 3.
2.2 抖动防护 (Thrashing prevention)
个人电脑对系统抖动(内存颠簸)更加敏感,因为抖动会导致界面卡顿(UI 渲染延迟),从而严重影响用户体验。多代 LRU 为广大没有安装 oomd 的笔记本和台式机用户提供了抖动防护功能。
用户可以向 min_ttl_ms 写入数值 N,以防止最近 N 毫秒内活跃的工作集(working set)被回收。如果内存无法容纳该工作集,则会触发 OOM 杀手(OOM killer)。换句话说,这个选项就像一个可调节的压力释放阀,当它打开时,会终止那些大概率当前没有被使用的应用程序。
根据人类可感知的平均延迟(约 100 毫秒),设置 N=1000 通常就能消除因系统抖动导致的无法忍受的卡顿。设置更大的值(如 N=3000)虽然能让卡顿变得更不明显,但也会带来提早触发 OOM 杀手的风险。
默认值为 0,表示禁用该功能。
3. 实验性特性
/sys/kernel/debug/lru_gen 接受以下各小节中描述的命令。支持输入多行命令,也支持使用逗号 , 和分号 ; 进行拼接。
/sys/kernel/debug/lru_gen_full 提供了用于调试的额外统计信息。配置 CONFIG_LRU_GEN_STATS=y 可以在该文件中保留已被回收的“代”(generations)的历史统计数据。
3.1 工作集评估 (Working set estimation)
工作集评估用于测量应用程序在给定时间间隔内需要多少内存,这种测量通常对应用程序本身的性能影响极小。例如,数据中心希望优化任务调度(装箱算法/bin packing)来提高内存利用率。当有新任务进入时,任务调度器需要先了解其管理的各台服务器是否有能力为该新任务分配一定数量的内存,然后才能选出合适的候选服务器。为此,任务调度器需要对现有任务的工作集进行评估。
读取 lru_gen 时,它会返回每个内存控制组(memcg)和节点在不同时间间隔内被访问的页面数量的直方图。MAX_NR_GENS 决定了每个直方图的柱数(bins)。这些直方图是非累加的。
memcg memcg_id memcg_path
node node_id
min_gen_nr age_in_ms nr_anon_pages nr_file_pages
...
max_gen_nr age_in_ms nr_anon_pages nr_file_pages
每个柱(bin)包含了在 age_in_ms 时间内被访问过的预估页面数量。例如,min_gen_nr 包含最冷(最久未访问)的页面,而 max_gen_nr 包含最热(最近频繁访问)的页面,因为前者的 age_in_ms 最大,后者的 age_in_ms 最小。
用户可以向 lru_gen 写入以下命令来创建一个新的代 max_gen_nr+1:
+ memcg_id node_id max_gen_nr [can_swap [force_scan]]
can_swap 默认取决于系统的 swap 设置;如果将其设置为 1,则会在 swap 关闭时强制扫描匿名页面,反之亦然。force_scan 默认值为 1;如果将其设置为 0,则会启用启发式算法以减少开销,但这也可能会降低扫描的覆盖范围。
一个典型的应用场景是:任务调度器每隔一定的时间间隔运行此命令来创建新的“代”,并根据这些服务器在当前时间间隔内定义的冷页面大小,对它所管理的服务器进行排序。
3.2 主动回收 (Proactive reclaim)
主动回收是指在系统没有内存压力时诱发页面回收。它通常只针对冷页面。例如,当一个新任务进来时,任务调度器希望主动回收其所选服务器上的冷页面,以提高成功部署该新任务的概率。
用户可以向 lru_gen 写入以下命令,来回收小于或等于 min_gen_nr 的代:
- memcg_id node_id min_gen_nr [swappiness [nr_to_reclaim]]
min_gen_nr 应当小于 max_gen_nr-1,因为 max_gen_nr 和 max_gen_nr-1 尚未完全老化(相当于活跃列表/active list),因此不能被回收。swappiness 会覆盖 /proc/sys/vm/swappiness 中的默认值。nr_to_reclaim 限制了要回收的页面数量。
一个典型的应用场景是:任务调度器在尝试将新任务部署到某台服务器之前运行此命令。如果由于预估过高而未能释放出足够的冷页面,它会根据工作集评估阶段得到的排序结果,在下一台服务器上重试。这种相对温和(非强制)的方法可以限制对现有任务造成的影响。
posted on 2026-10-02 16:45 Hello-World3 阅读(7) 评论(0) 收藏 举报
浙公网安备 33010602011771号