Linux 内核内存碎片整理与反碎片化机制:从伙伴系统到页面迁移的实战解析
在长时间运行的高性能服务器上,即使物理内存充足,分配连续大页(HugePage、DMA 区域)仍然可能失败。其根本原因并非真正的内存耗尽,而是内存碎片化。Linux 内核通过一套精密的内部分配器、迁移类型标记和 Compaction 机制来解决这一顽疾。本文将深入剖析伙伴系统的反碎片化设计、页面迁移策略,以及生产环境中如何诊断和缓解碎片问题。
一、伙伴系统与碎片化本质
1.1 Buddy System 的工作方式
Linux 内核的物理内存管理以页(Page,通常 4KB)为单位,由伙伴系统(Buddy Allocator)维护。它将空闲页面按 2 的幂次分组(order-0 即 1 页、order-1 即 2 页、order-9 即 2MB),相同大小的链表串联。
分配时,若目标阶没有空闲块,会从更高阶"劈开"一半给请求者,另一半归入低阶链表;释放时,若"伙伴"也空闲,则向上合并。这种高效的大小劈开/合并机制天然蕴含碎片化风险——随机大小分配会将高阶块不断打碎。
1.2 外部碎片 vs 内部碎片
人们常误以为外部碎片是"内存不连续",实际上真正的外部碎片问题在于无法满足连续大块内存请求。即使总空闲内存充足,若它们零散分布在非连续的物理页上就无济于事。内部碎片则是分配单元内部未被利用的冗余空间(如申请 5KB 实际拿到 8KB)。本文聚焦外部碎片。
1.3 为什么碎片化危害严重
DMA 设备要求连续物理页,大页(HugePage)必须由 order-9(2MB/4MB)的连续块组成,用户态 mmap 大缓冲区触发透明大页(THP)提升分配压力,内核模块(驱动固件加载)也需要连续物理区域。碎片一旦形成,这些请求就只能降级或失败。
二、反碎片化设计:可移动性分组(Migratetype)
2.1 核心洞察:按"可移动性"分类
2005 年 Mel Gorman 提交的 anti-fragmentation patch 引入关键思想:将页面按其迁移能力归类——可回收页(RECLAIMABLE,文件缓存) 聚集后可通过回收释放整块,可移动页(MOVABLE,用户态匿名页、大多数内核分配) 聚集后可通过页面迁移腾出连续空间,不可移动页(UNMOVABLE,内核 slab 分配、mlock 锁定页) 是碎片的"钉子户"。
2.2 分配区的 Migratetype 分组
内核将内存划分为 MAX_ORDER 个块(通常 11 个,共覆盖 0~1023 页),每个块头部用 free_list 按 migratetype 分组。include/linux/mmzone.h 中关键的迁移类型:
enum migratetype {
MIGRATE_UNMOVABLE, // 内核数据结构,无法迁移
MIGRATE_MOVABLE, // 用户页、可移动分配
MIGRATE_RECLAIMABLE, // 可回收(文件缓存)
MIGRATE_PCPTYPES, // Per-CPU 热页缓存
MIGRATE_ISOLATE, // 隔离用(不参与分配)
MIGRATE_TYPES
};
2.3 分配策略:定向匹配
默认策略是"按请求类型从对应 migratetype 分配"。高优先级的可移动请求不会污染不可移动池。例如 kmalloc(内核小对象)标注为 UNMOVABLE,alloc_pages(用户缓冲区)标注为 MOVABLE。这种分配隔离确保不可移动页在初期不会散布在可移动区域。
2.4 Per-CPU 页缓存(PCP)
每个 CPU 维护一个热页小池,缓存单页分配/释放以加速高频操作。PCP 内部也按 migratetype 分桶,避免在热路径上引入碎片。
三、主动 Compaction:规整内存
3.1 触发条件
当高阶分配连续失败次数超过阈值,或管理员显式触发 /proc/sys/vm/compact_memory 时,Compaction 被唤醒。此外 THP khugepaged 守护进程在后台尝试将分散小页合并为大页。
3.2 Compaction 核心流程
mm/compact.c 中的 compact_zone() 是核心执行函数,分为两个扫描阶段:
- 正向扫描(块起始→结尾):寻找连续可迁移区域中"足够大"的起点(
fast_find_migrateblock) - 反向扫描(块结尾→起始):在目标区域反向定位空闲/可回收页用于接收迁移页
- 页面迁移(migrate_pages):将源区可移动页复制到目标区空闲块
// mm/compact.c 核心结构
struct compact_control {
struct list_head freepages; // 找到的空闲页
struct list_head migratepages;// 待迁移页面
unsigned long nr_migratepages;
unsigned long nr_freepages;
unsigned long free_pfn; // 扫描开始位置(找空闲)
unsigned long migrate_pfn; // 扫描开始位置(找可迁移)
enum migrate_mode mode; // SYNC / ASYNC
};
3.3 轻量 vs 全量 Compaction
- 异步(ASYNC):轻量级,限时运行,允许中断,由 khugepaged 在 THP 分配时触发
- 同步(SYNC):重量级,扫描整个 zone,不妥协直到完成或真正失败,在高阶分配紧急失败时触发
异步模式通过 compact_zone(CC, M_ASYNC) 调用,内核通过 compact_result 状态追踪效果(如 COMPACT_SKIPPED 跳过无法处理的 zone)。
3.4 watermark_boost
连续失败多次后临时提高水位(watermark),给 Compaction 更大倾斜空间,避免过早启动直接回收(reclaim)或 OOM。watermark_boost 让系统宁可多花时间做回收/规整,也不直接陷入 swapping。
四、页面迁移机制详解
4.1 页面迁移的本质
Compaction 的核心难点不在扫描,而在于安全地移动一个正在被使用的页。此操作需同步反向映射(rmap):所有映射了此页的进程页表项都必须原子更新为指向新位置。
4.2 反向映射(Reverse Map)
匿名页通过 anon_vma 链表追踪映射了它的 VMA(虚拟内存区域)。文件页通过 address_space 的优先搜索树(prio_tree)追踪。迁移时需锁定页面、隔离 LRU 缓存、遍历 rmap 锁住所有 PTE 并将其指向新页:
// 伪代码示意
for (each page->mapping holder) {
pte = get_pte(vma, addr);
lock_pte(pte);
new_pte = pte_mkold(pte); // 清除 Accessed
set_pte(pte, pfn_pte(new_pfn)); // 指向新物理页
flush_tlb(vma, addr);
}
4.3 unmap_and_move
mm/migrate.c 中 unmap_and_move() 是实际执行迁移的函数。步骤如下:
- try_to_unmap:对所有持有者执行反向映射解除(清除 PTE Present 位,页面仍在 LRU)
- 如果仍有人持有(trylock_page 失败),放弃迁移
- 从源 LRU 隔离,放到迁移队列
- 取目标空闲页,复制内容(move_to_new_page)
- 修复所有 PTE 指向目标页(通过 rmap_walk)
不可迁移页(如 kernel buffers、mlock 锁定页)被发现时直接跳过,这解释了为何 slab 密集分配的服务器更容易碎片化。
五、伙伴系统初始化与 Zones
5.1 Zone 的历史与现状
传统的 DMA/DMA32/Normal/HighMem 分层已随 64 位演进简化为 ZONE_DMA(兼容性保留)、ZONE_NORMAL、ZONE_MOVABLE。ZONE_MOVABLE 是反碎片化的利器——它是纯粹的可移动页池,用于确保总可移动量有下界。
# 查看 zone 信息
cat /proc/zoneinfo | grep -E "Node|zone|managed|present"
5.2 Movable Zone 的精确计算
movable_zone = max(所有 node 的 movable 管理页面),管理员可通过 kernelcore= 启动参数强制指定不可移动区域大小,剩余分配给 ZONE_MOVABLE。这是 HPC 和 DPDK 用户常用手段。
六、生产诊断:量化碎片化程度
6.1 Buddy 信息分析
# 查看 order 0~10 的空闲页数
cat /proc/buddyinfo
# 示例:
# Node 0, zone Normal 283 534 1123 456 223 105 67 34 18 5 2
# ^^^ ← order-7 仅 34 块
低阶丰富、高阶稀缺是碎片化的典型特征。
6.2 Compaction 统计
# 检查 compaction 计数
cat /proc/vmstat | grep compact
# compact_stale — 过期扫描
# compact_fail — 迁移失败
# compact_success — 成功规整
# compact_migrate_scanned / compact_free_scanned — 扫描页数
成功/失败比 >3 表示 compaction 工作正常;若失败率高且大量因 migration 失败,说明有太多 UNMOVABLE 页散布。
6.3 Fragmentation Index
mm/page_alloc.c 提供碎片指数函数 fragmentation_index():
- -1 表示无法找到指定阶空闲块
- 0 表示足够块可用
- 接近 0 的小正值表示轻微碎片
也可以通过 sysfs 或工具直接评估:
# 使用 numactl 或 thp 系统监控
cat /sys/kernel/mm/transparent_hpage/khugepaged/pages_to_scan
watch -n 1 "cat /proc/buddyinfo"
七、生产环境碎片化治理策略
7.1 内核参数调优
# 主动 compact 的积极性(默认 500,范围 0~1000,越大越主动)
sysctl -w vm.extfrag_threshold=200
# compaction 失败后的重试间隔(ms)
sysctl -w vm.compact_unevictable_allowed=1
# 每次 compact 扫描页数 / 跳过页数
sysctl -w vm.compaction_proactiveness=20 # 5.18+ 新增,主动 compact 力度
# 预留低阶页压力阈值
sysctl -w vm.min_free_kbytes=65536
7.2 THP 配置选择
| 场景 | 建议 |
|---|---|
| HPC、DPDK、Redis | never,预分配静态大页 at boot |
| 通用数据库(MySQL/PG) | madvise,关键段 madvise 申请 |
| 无 NUMA 敏感业务 | always(默认) |
| 阶数稀缺场景 | 通过 khugepaged/scan_sleep_millisecs 提高扫描频率 |
# 关闭 THP 降低碎片风险(高并发时也可避免 khugepaged CPU 开销)
echo never > /sys/kernel/mm/transparent_hpage/enabled
# 静态大页(启动参数)
hugepagesz=2M hugepages=2048
7.3 NUMA 与 movable core
多 NUMA 节点场景,碎片更容易跨节点堆积。建议:
- 用
numactl --membind绑定关键应用节点 - 使用
kernelcore=预留每 node 非 movable 底量 - 开启 CONFIG_ZONE_MOVABLE(默认开)
- 关闭 CONFIG_COMPACTION 以外的不必要 zone 间碎片化配置
7.4 应用层配合
- 预分配池:启动时集中分配大块 slab 缓存,减少运行时 slab 蔓延
mlock/固定内存减少:UNMOVABLE 页越多越难规整- madvise(MADV_HUGEPAGE):主动告诉内核哪些段需要大页,让内核提前规整
- 对齐大块 mmap:避免随机小分配把 zone 打散
八、内核演进:新机制展望
8.1 proactive compaction(5.9+)
引入 proactiveness 参数,由后台线程周期性主动做轻量 compact,避免碎片累计到临床失败才被动应对。
8.2 巨型页碎片销毁(demotion,5.15+)
当 THP 无法维持,内核可拆分为小页加入 buddy 系统缓解压力,反向操作也支持——小页池充裕时重新聚合。这种"流动性"使大页管理更具弹性。
8.3 CXL 内存与 zone 弹性
CXL 类型3内存(内存池)加入系统后,可设置为 ZONE_MOVABLE,既扩充池子又不过度污染内核可移动性分类。
8.4 PSI(Pressure Stall Information)
/proc/pressure/memory 监控实时内存碎片压力,对 some avg10 突升及时告警,配合 proactive compaction 形成自动化应对。
九、实战案例:DPDK 服务器碎片化解决
某金融 HFT 服务器运行 DPDK + 大页内存分配,运行 72 小时后频繁报"Cannot allocate contiguous memory"。观察如下:
$ cat /proc/buddyinfo
Node 0, zone Normal 9132 5312 2147 896 256 67 18 2 0 0 0
order-7 以上几乎为 0,且 compact_fail 高。最终解决:
- 启动参数加
hugepagesz=1G hugepages=16(预分配 16GB 硬件大页,不归 buddy 管理) - 关闭 THP
echo never > /sys/.../transparent_hpage/enabled - 将 DPDK 绑定到 ZONE_DMA32 之外的核心内存区
- 关键守护进程用完即释放非关键大对象,避免 slab UNMOVABLE 堆积
+72h 后归零 OOM 分配失败事件。
总结
Linux 内核通过 Migratetype 分组将"可移动"隔离,用 Compaction + rmap 迁移机制动态整理碎片,辅以 zone 分层和 watermark 弹性,构成了完整的反碎片化体系。运维的核心思路是:
- 减少 UNMOVABLE 散布(控制 slab、不用不必要的 mlock)
- 预分配大页给关键路径(避免实时竞争)
- 主动(而非被动)触发 Compaction
- 持续观察 buddyinfo + vmstat compact 计数器
碎片化本质是时间与空间的博弈,了解这套机制,才能在毫秒级系统中让大页分配"指哪打哪"。

发表评论 取消回复