引言

Linux 内核的内存管理系统是整个操作系统的核心组件之一。当系统内存紧张时,内核必须在有限的时间内做出关键决策——如何回收内存、选择哪些页面换出、以及最后关头如何处理 OOM(Out-Of-Memory)状况。本文将从内核源码层面深入解析 Linux 的页面回收机制、LRU 算法演进、页面置换策略,以及 OOM Killer 的决策逻辑,并结合 2026 年最新内核特性进行分析。

一、页面回收(Page Reclaim)的整体架构

Linux 内核的页面回收路径涉及多个子系统的紧密协作。当系统内存低于特定阈值时,内核会通过以下路径触发回收:

kjournald / userspace process
    ↓
balance_pages() [mm/vmscan.c]
    ↓
kswapd (后台守护进程)  ← →  直接回收 (Direct Reclaim)
    ↓
shrink_node() -> shrink_lruvec() -> shrink_list()

全局内存回收的核心入口位于 mm/vmscan.c,其中定义了三个关键水位标记(watermark):

  • WMARK_MIN(min):内存紧急阈值,低于此值系统将阻塞进程进行直接回收
  • WMARK_LOW(low):kswapd 唤醒阈值,后台守护进程开始异步回收
  • WMARK_HIGH(high):系统健康阈值,kswapd 认为内存充足后可休眠

这些水位由内核根据各 NUMA 节点的物理内存总量动态计算,init_per_zone_wmarks() 函数将各区的最小空闲内存值按比例分配到三个水位,确保大内存节点和小内存节点均能得到合理的回收触发点。

二、LRU 算法的演进:从传统 LRU 到多级 LRU

2.1 传统 LRU 的局限性

经典 LRU(Least Recently Used)使用两个链表(active/inactive)维护页面热度。但这种方法存在两个根本问题:

  1. 冷热页面判断滞后:页面首次加入 inactive 链表,只有在第二次访问时才能提升到 active,这意味着单次扫描页面可能永远无法进入 active 列表
  2. 链表维护开销大:在高内存系统中,LRU 链表可能包含数百万个页面,链表操作本身就是瓶颈

2.2 Multigen LRU(MGLRU):Linux 6.1+ 的革命性突破

自 6.1 版本起引入选项式的 Multigen LRU,由 Google 工程师 Yu Zhao 主导开发。它解决了传统 LRU 的固有缺陷:

// 核心数据结构:多代 LRU
struct lru_gen_folio {
    unsigned long blooms[BLOOM_FILTER_SIZE];  // Bloom 过滤器快速判断访问
    unsigned long folios[EVICTION_SIZE];       // 待驱逐页面候选
    int nr_folios;                              // 候选数量
    unsigned int gen;                            // 当前代数
};

MGLRU 的核心理念是用年龄(generation)取代精确的访问顺序:

  • 新页面加入最年轻的 Generation(gen 0)
  • 周期性"老化":每次扫描将所有页面 age+1,但在活跃访问路径上重置回当前 gen
  • 驱逐时优先回收最老 generation 中的页面
  • 用 Bloom 过滤器以 O(1) 时间复杂度跟踪页面访问历史,无需全链表扫描

Google 内部大规模部署数据显示,MGLRU 将页面失效率(page fault rate)降低了约 25%,在数据库负载和容器密度场景下效果尤为显著。

2.3 Bloom 过滤器的妙用

MGLRU 在 Bloom 过滤器中编码页面的多维历史信息——不仅记录"是否被访问",还记录"在第几代被访问"。这使得驱逐决策不仅考虑页面最近使用时间,还综合考虑页面的访问频率和模式,比传统 LRU 的冷热二分法更加精细。

// Bloom 过滤器判断页面是否在某一代被访问过
static bool lru_gen_test_access(struct lruvec *lruvec, unsigned long nr_pages, int gen)
{
    struct lru_gen_folio *lrugen = &lruvec->lrugen;
    // Bloom 查找: O(1) 检查页面在第 gen 代是否被引用
    return test_page_bloom(lrugen, nr_pages, gen);
}

三、页面类型与回收策略

3.1 匿名页 vs 文件页

Linux 将待回收页面分为两大类,采用截然不同的回收路径:

  • 匿名页(Anonymous Page):来源于进程堆/栈/匿名 mmap 映射。脏页必须写入 Swap 设备后才能释放,回收优先级较低(换出代价高),扫描成本低(无需 I/O)
  • 文件-backed 页(Page Cache):来自文件 mmap 或 read/write 缓冲。干净页可直接丢弃回收,回收优先级较高,脏页扫描可能引发 I/O 等待

内核通过 get_scan_count() 计算对 folio 类型列表的扫描强度比值(对应 /proc/sys/vm/swappiness)。在数据库服务器上通常设置 swappiness=1,尽量避免匿名页换出。

3.2 Folio 机制:Linux 5.16+ 的复合页抽象

2022 年引入的 folio 机制统一了 compound page 的接口,使基础内存操作统一在"可能的大页"层面进行,降低了 LRU 链表 .scan 中频繁 split/join 的 overhead。2026 年,几乎所有 core VM 路径(swap、page cache、THP)已完全迁移到 folio 入口。

四、Swap 系统与 zRAM/zSwap

4.1 zSwap 内存压缩缓冲

zSwap 将待换出的页面压缩后存储在内存缓冲区中,只有当缓冲区满后才真正写入磁盘 Swap。在内存受限的嵌入式/容器场景中,额外的内存通过压缩比率换取(典型压缩比 2:1 到 3:1),有效减少 I/O 阻塞。

4.2 Swap Readahead 自适应关闭

在 NVMe/SSD Swap 上,预读(swapin_readahead)的正收益减小,反而可能引入额外的 I/O 碎片。Linux 6.6+ 内核将此策略优化为仅在顺序访问模式下启用,随机访问模式直接逐页换入,避免无效 I/O。

五、Shrinker 与可回收内核内存

5.1 shrink_slab 路径

当匿名页和文件页仍无法满足需求时,内核会调用注册在各子系统的 shrinker,回收以下内核内存:

  • dentry cache / inode cache:目录项和 inode 条目缓存,通过 slabtop 可观察其大小
  • BTRFS/XFS 元数据:文件系统专用缓存
  • TCP/drm/i915:特定驱动注册的内核内存池
  • bpf/ksm 等子系统:日益增多的内核子系统注册 shrinker

5.2 cgroup-aware Shrinker(Linux 6.7+)

新式 shrinker 从全局链表迁移到 cgroup 感知结构体,使容器环境中的内存回收能够独立地为每个 cgroup 计算压力并维护公平性,解决了全局 shrinker 导致容器间接互相干扰的问题。

六、OOM Killer 及其替代路径

6.1 OOM Badness 评分机制

当系统彻底无法回收足够内存时,out_of_memory() 调用 oom_badness() 函数对每个进程评分:

  • 基础分由 RSS + Swap 占用决定,长期运行进程通过平方根函数增速放缓
  • 特权级调整:SYS_ADMIN / SYS_RAWIO 进程轻微保护(分数降低 3%)
  • oom_score_adj:用户可配置偏移量 [-1000, 1000],-1000 表示完全豁免 OOM 终止

6.2 PSI:用压力信息取代 OOM 滞后响应

Linux 4.20 引入的 Pressure Stall Information (PSI) 改变范式:不再等 OOM 发生,而是在任务被阻塞前就采取预防措施。通过 cgroup v2 的 memory.pressure 配置阈值并注册 eventfd 通知,可以在真正的失速触发前进行干预——驱逐容器、调度降级、弹性伸缩。

七、2026 年新趋势:Container-Aware Reclaim

7.1 MGLRU + Memory Tiering 联合调度

Linux 7.0 引入flat tier demotion:多级内存(DRAM + CXL 或 PMem)的战略降级与 MGLRU 的页面 age 机制直接打通。gen_age 直接决定页面应降级到哪一 Tier,不再需要启发式扫描判断热度,大幅降低了 Tier 间迁移的延迟和不确定性。

7.2 Memory Qos 云原生化

6.13 内核中的 Memory Quality of Service 接口已作为虚拟设备在多个公有云厂商暴露,允许 guest VM 向 Hypervisor 反馈其内存使用模式,与 QEMU virtio-mem 联动完成产品化落地。

八、实战性能调优速查表

不同场景的推荐 sysctl 配置:

  • 数据库服务器:vm.swappiness=1、vm.vfs_cache_pressure=50、vm.watermark_scale_factor=200(更积极回收文件页)
  • 容器/Pod 环境:vm.swappiness=0、vm.vfs_cache_pressure=100、cgroup v2 设置 memory.low 保障关键容器带宽
  • 桌面/开发机:vm.swappiness=60(默认)、MGLRU enabled、CPU 密集型 workload 设置 oom_score_adj=-500 保护编译进程
  • 大内存服务器(64GB+):vm.swappiness=10、vm.vfs_cache_pressure=200、vm.watermark_scale_factor=50(减少后台回收开销)

结语

理解并优化 Linux 内核的内存回收机制,是高性能系统运维和工程化的基石。从传统 LRU 的双链表到 MGLRU 的 Bloom 过滤器与多代年龄模型,再到 PSI 压力预判机制和 cgroup-aware 公平回收,Linux 内核的内存管理策略正变得越来越精细、可预测。在容器化、异构内存等新硬件拓扑下,内存回收不再是单一阈值问题,而是一个涉及多层级、多维度的系统工程。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部