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)。核心规则是:
- 分配从小阶开始,向上拆分
- 释放时检查 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 的工作流程:
- 从 zone 低端向高端扫描 MOVABLE 页面
- 从 zone 高端向低端扫描 Free 页面
- 每一轮发现一个 MOVABLE 页面,就尝试将其迁移到一个 Free 页面
- 不进行页面回收(no reclaim),因此速度快但对内存压力不敏感
3.2 Slow Scanner(同步完整扫描)
当 Fast Scanner 无法找到足够的连续页面时,触发 Slow Scanner:
- 扫描 zone 中所有页面,包括 RECLAIMABLE
- 可能触发 kswapd 回收页面
- 尝试更激进的迁移(即使页面被 mmap 锁定也会等待)
- 对应
/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

发表评论 取消回复