Linux 内核内存压缩(Compaction)与碎片整理工程实战:从 Buddy 分配器到大页可用性优化

一、问题定义:为什么物理内存碎片仍是云基础设施的隐形杀手

现代云服务器的内存配置日益膨胀——256GB、512GB 甚至数 TB 的 DRAM 已是常态。然而,随着系统运行时间延长,物理内存碎片问题开始显现其破坏力:

  • 大页(Hugepage)分配失败:即使仍有数十 GB 空闲内存,2MB/1GB 大页仍可能因为物理地址不连续而分配失败
  • 设备 DMA 失败:某些硬件需要连续物理内存块,碎片超过阈值即触发 I/O 错误
  • NUMA 本地性退化:碎片化加剧了跨 NUMA 节点访问,导致延迟抖动
  • KSM 效率降低:碎片化让更多页面分散在不同区域,扫描开销上升

理解并控制物理内存碎片,是每一位 Linux 内核工程师和 SRE 的必修课。本文将从 Buddy 分配器的设计局限出发,深入剖析 compaction 算法的两个扫描器、与 CMA/THP 的交互机制,并给出生产环境中的监控调优实操。

二、碎片产生的根本原因:Buddy 分配器的"抽屉原理"

Linux 使用 Buddy 分配器管理物理页面。它将内存划分为不同阶(order)的连续页面块(0 阶 = 4KB,1 阶 = 8KB,... 9 阶 = 2MB)。核心规则是:

  1. 分配从小阶开始,向上拆分
  2. 释放时检查 Buddy 是否空闲,能合并就向上合并

经过大量分配释放后,高阶连续块逐渐耗尽,被"钉子"(不可移动页面)分割:

内存布局示意(每个格子 = 4KB 页面):

Node 0 Zone Normal:  10GB
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│F │U │F │F │U │F │F │F │U │F │F │U │F │F │F │U │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
  F = Free    U = Unmovable (内核栈/页表/DMA缓冲)

最大连续块 = 3 pages (12KB),无法分配 order-2 (16KB)

Linux 根据页面可迁移性将页面分为四类(/proc/pagetypeinfo):

  • UNMOVABLE:内核栈、页表、DMA 缓冲区,无法迁移
  • MOVABLE:用户空间页面、页面缓存,最容易迁移
  • RECLAIMABLE:可回收但不能直接分配给用户的页面(如 slab)
  • RESCUED(特定场景)

碎片化的本质是:MOVABLE 页面被 UNMOVABLE 页面隔离,Compaction 的工作就是把这些 MOVABLE 页面搬走,腾出连续空间。

三、Compaction 双扫描器架构

Linux 内核的 compaction 由两个协作的扫描器组成,它们在 /mm/compaction.c 中实现:

3.1 Fast Scanner(异步快速扫描)

快速扫描器从 zone 的两端相向扫描,寻找可以迁移的页面:

// mm/compaction.c - 核心数据流
struct compact_control {
    struct list_head freepages;      // 空闲页面链表
    struct list_head migratepages;   // 待迁移页面链表
    unsigned long nr_migrate;        // 待迁移计数
    unsigned long nr_freepages;      // 空闲页面计数
    int order;                       // 目标分配阶
    int migratetype;                 // 迁移类型
    bool fast_only;                  // 仅快速模式
};

Fast Scanner 的工作流程:

  1. 从 zone 低端向高端扫描 MOVABLE 页面
  2. 从 zone 高端向低端扫描 Free 页面
  3. 每一轮发现一个 MOVABLE 页面,就尝试将其迁移到一个 Free 页面
  4. 不进行页面回收(no reclaim),因此速度快但对内存压力不敏感

3.2 Slow Scanner(同步完整扫描)

当 Fast Scanner 无法找到足够的连续页面时,触发 Slow Scanner:

  1. 扫描 zone 中所有页面,包括 RECLAIMABLE
  2. 可能触发 kswapd 回收页面
  3. 尝试更激进的迁移(即使页面被 mmap 锁定也会等待)
  4. 对应 /proc/sys/vm/compact_unevictable 控制

3.3 扫描模式

内核定义了三种扫描模式:

模式 触发条件 开销
COMPACT_PRIO_SYNC_FULL 分配失败后手动触发 最大:完整扫描 等待
COMPACT_PRIO_SYNC_LIGHT 分配紧迫但非阻塞 中等:跳过 UNEVICTABLE
COMPACT_PRIO_ASYNC 定时后台触发 最小:快速扫描

四、Compaction 的触发时机

4.1 分配路径中的直接触发

// mm/page_alloc.c 中的调用链
alloc_pages()
  → __alloc_pages_nodemask()
    → __alloc_pages_direct_compact()  // gfp_flags 允许阻塞时
      → try_to_compact_pages()
        → compact_zone()

当分配请求的 order

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部