Linux内核Memory Compaction(内存规整)深度工程实战——从碎片整理到大页分配的完整链路
摘要:在Linux内核物理内存管理中,Buddy System解决了大块内存的分配与回收,但长期运行后外部碎片(External Fragmentation)问题必然出现。Memory Compaction(内存规整)是内核为应对碎片问题引入的核心机制,它通过迁移可移动页面来整合出连续的物理空间。本文将系统拆解compaction的完整链路——从碎片检测、扫描策略、页面迁移,到THP分配保障与生产环境调优,帮助读者建立从理论到工程实践的完整认知。
一、问题的本质:外部碎片(External Fragmentation)
假设系统运行一段时间后,物理内存呈现如下分布:
物理地址:0x0000 ────────────────────────────── 0xFFFF
┌──────┬──────┬──────┬──────┬──────┐
│ 空闲 │ 已用 │ 空闲 │ 已用 │ 空闲 │
│ 4页 │ 8页 │ 4页 │ 4页 │ 8页 │
└──────┴──────┴──────┴──────┴──────┘
总空闲:20页 | 最大连续块:8页(无法分配order=4的16页)
虽然总空闲页面充足,但由于已用页面将空闲区域分割,无法满足高阶(high-order)连续内存分配请求。这就是外部碎片。
碎片产生的根本原因:
- 内存分配/释放的时序随机性
- 页面生命周期差异(内核长生命周期页面 vs 用户态短生命周期页面交错分布)
- DMA/KSWAPD等内核线程的固定分配模式
- SLAB分配器保留的不可移动对象
二、Compaction 核心原理:以迁移换连续
Compaction的核心思路是不依赖释放,而是主动迁移。它将分散的可移动页面集中到内存区域的一端,另一端则形成连续的大块空闲内存:
Compaction前:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ Mov │ UnMv │ Mov │ UnMv │ Mov │ UnMv │
│可移动│不可移│可移动│不可移│可移动│不可移│
└──────┴──────┴──────┴──────┴──────┴──────┘
Compaction后:
┌─────────────────────────────────────────────┬──────────┐
│ 可移动页面(集中到前端) │ 大块空闲 → 可供高阶分配
└─────────────────────────────────────────────┴──────────┘
↑ 不可移动页面留在原位,可移动页面绕过它们聚集
2.1 三大核心阶段
一次完整的 compaction 操作分为三个阶段:
| 阶段 | 函数 | 职责 |
|---|---|---|
| 1. 快速扫描 | fast_isolate_freepages() / compactionzone() | 在zone中快速识别可迁移的空闲页面作为"目标" |
| 2. 反向扫描 | migrate_scan_reverse = 1 时从zone末尾开始扫描 | 从zone低端向高端扫描,找到可移动的页面作为"源" |
| 3. 页面迁移 | migrate_pages() | 将"源"页面内容复制到"目标"空闲页面,更新页表映射 |
2.2 页面可迁移性分类
内核根据页面属性决定能否迁移:
页面类型 │ 可迁移? │ 说明
──────────────────┼─────────┼────────────────────────
用户态匿名页面 │ ✅ │ 最频繁的迁移对象
文件映射页面 │ ✅ │ 写回后迁移
共享内存页 │ ✅ │ shm/tmpfs页面
内核SLAB可回收页 │ ✅ │ 部分slab对象
页面缓存(PageCache)│ ⚠️ │ 干净页可直接迁移
匿名大页(THP) │ ✅ │ 拆分后逐页迁移
内核代码/数据 │ ❌ │ 不可移动
不可回收内核页 │ ❌ │ pinned页面
硬件DMA区域 │ ❌ │ 设备驱动锁定
三、核心数据结构与算法
3.1 区域扫描器(struct compact_control)
每次 compaction 操作通过 struct compact_control 协调扫描器行为:
struct compact_control {
struct list_head freepages; // 收集到的空闲页面链表
struct list_head migratepages; // 收集到的可迁移页面链表
unsigned long nr_migratepages; // 已收集的可迁移页面数
unsigned long nr_freepages; // 已收集的空闲页面数
unsigned long free_pfn; // 扫描器1:空闲页面扫描PFN起点
unsigned long migrate_pfn; // 扫描器2:可迁移页面扫描PFN起点
unsigned long fast_start_pfn; // fast scan起始位置
unsigned int order; // 目标分配的阶
enum migrate_mode mode; // 迁移模式
int order_status; // 最终结果状态
// ...
};
两个扫描器协同工作:空闲页面扫描器寻找"落脚点",可迁移页面扫描器寻找"源头"。
3.2 碎片索引(Fragmentation Index)
内核通过碎片指数评估zone是否值得执行compaction。关键函数:
// 计算碎片指数
int fragmentation_index(struct zone *zone, unsigned int order)
{
// 统计每个order下的空闲页面数
// 碎片指数 = (尝试分配的order成功概率)
// 通常:order 0-9成功被认为不值得compact
// 只有高阶(order >= 10, 即4MB)失败时才触发
ffs = 0;
for (o = order; o < MAX_ORDER; o++) {
free = zone_page_state(zone, NR_FREE_PAGES_O(o));
// 关键公式:如果当前order以下所有空闲页
// 加起来都不够order阶,则认为需要compact
}
}
碎片指数定义:
- -1000:完全无碎片,无需compaction
- 0:刚好需要compaction的临界点
- 1000:极度碎片化,compaction代价极高
3.3 迁移模式
内核支持三种compaction模式,激进程度递增:
COMPACT_PRIO_SYNC_FULL(同步全力):直接compaction,完成全部可能的迁移尝试,用于THP分配失败时紧急扩容。
COMPACT_PRIO_SYNC_LIGHT(同步轻量):限制扫描范围,适用于GFP_KERNEL路径下的直接回收中。
COMPACT_PRIO_ASYNC(异步):kcompactd后台线程执行,仅做轻量级碎片整理。
四、触发路径与时序
4.1 主动Compaction路径
- 直接分配失败:__alloc_pages_slowpath() → try_to_compact_pages()
- THP分配保障:khugepaged 后台碎化收缩时,或 THP fault 分配失败时触发
- hugetlb分配:大页分配器无法满足需求时触发紧急compaction
- 用户触发:echo 1 > /proc/sys/vm/compact_memory
4.2 后台异步Compaction
每个NUMA node有一个 kcompactd 守护线程,周期性检查碎片水平:
// kcompactd工作循环
while (!kthread_should_stop()) {
// 等待唤醒或被周期性超时启动
wait_event_freezable(kcompactd_wait, ...);
// 检查各zone的碎片水平
pgdat_balanced(); // 评估是否需要compact
// 根据碎片阈值决定是否compact
if (碎片水平 > 阈值) {
compact_zone();
}
// 默认休眠(通过compact_period配置)
}
kcompactd 唤醒条件:
- wakeup_kcompactd():当高阶分配快速失败时,异步唤醒kcompactd
- 周期性超时启动(默认禁止周期性运行,仅在唤醒时执行)
4.3 直接回收中的Compaction
在内存压力较大的直接回收路径中,compaction可能不是最优选择(耗时),因此内核设有"避免compact"的判断:
// __alloc_pages_direct_compact()
// 只在 GFP_ATOMIC 以外的路径中尝试
// 且如果最近compaction失败过(COMPACT_SKIPPED),会直接跳过
五、页面迁移机制详解
5.1 迁移四步法
单个页面的完整迁移流程:
Step 1: unmap_and_move()
├── try_to_unmap() // 反向映射(RMAP)删除所有PTE引用
│ └── rmap_walk() // 遍历所有映射了该页的进程页表
│ └── try_to_unmap_one() // 清除PTE entry, 标记为swap条目
│
Step 2: __PageMovable判断
├── page->mapping->a_ops->migratepage() 存在?
│ ├── 匿名页:使用buffer_migrate_page
│ └── 文件页:使用migratepage回调
│
Step 3: 分配新页面
├── 从目标zone分配空闲页面(注意避免导致新的碎片)
│
Step 4: 内容复制 + PTE更新
├── move_to_new_page() // 复制页面内容
├── 获取页面锁(from_page和to_page)
├── copy_highpage() // 物理复制
└── 更新所有映射进程的PTE:new_page->pfn
5.2 关键优化:隔离而非强制
isolate_movable_page() 是迁移的前置条件:
- 成功:页面从LRU链表中摘除,可以进入迁移流程
- 失败:页面被锁或正在回写,跳过该页面(标记为不可迁移)
内核在迁移时是"尽力而为"(best-effort),不需要所有可迁移页面都成功迁移,只要有一半以上能迁移就可以形成足够大的连续块。
六、与THP(透明大页)的协同
6.1 THP碎片阈值
THP 分配需要 2MB 连续物理内存。为了避免无效的 compaction 尝试,内核引入了 khugepaged_max_pte_missed 和碎片阈值判断:
/sys/kernel/mm/transparent_hugepage/khugepaged/max_pte_missed
控制何时在离线路径上触发compaction以满足THP
/sys/kernel/mm/transparent_hugepage/defrag 的关键选项:
defrag = always → THP分配失败时立即触发直接compact
defrag = defer → 失败后只标记defer,由khugepaged延迟compact
defrag = madvise → 仅MADV_HUGEREGION地址区间触发compact
(内核 6.x新增)defrag = defer+madvise
6.2 khugepaged中的碎片收缩
除了分配时满足THP,khugepaged还有一个收缩碎片模式:在系统空闲时将小页合并为大页,触发compaction来创造连续空间。
七、性能监控与故障排查
7.1 /proc/vmstat 关键指标
# 查看compaction相关事件
cat /proc/vmstat | grep -i compact
compact_migrate_scanned ── 扫描的可迁移页面总数
compact_free_scanned ── 扫描的空闲页面总数
compact_isolated ── 尝试隔离的页面数
compact_stall ── 由于compaction导致任务阻塞的次数(重要!)
compact_fail ── compaction尝试失败次数
compact_success ── compaction成功次数
compact_kcompatd_wakeup ── kcompactd唤醒次数
compact_daemon_migrate_scanned ── kcompactd扫描的可迁移页面
compact_daemon_free_scanned ── kcompactd扫描的空闲页面
compact_pagemigrate_failed ── 页面迁移失败次数
compact_blocks_zero ── 扫描到零块空闲的次数
健康度判断:
stall / success比值过高:compaction代价大但收效低,建议调高碎片阈值fail >> success:大部分可移动页面不足,碎片不可挽回(考虑重启或内存回收)kcompatd_wakeup频繁:async compaction无法跟上碎片产生速度
7.2 /proc/buddyinfo 碎片可视化
# cat /proc/buddyinfo
Node 0, zone Normal 10 5 3 2 1 1 0 0 0 0 0 0 0 0 0
↑ ↑ ↑ ↑ ↑ ↑
4k 8k 16 32 64 128 ...KB
# 解读:如果order 9-11(2MB-4MB以上)全是0,而低端阶还很多
# → 存在严重外部碎片,compaction可能频繁失败
7.3 ftrace 跟踪compaction
# 开启compaction事件追踪
cd /sys/kernel/debug/tracing
echo 1 > events/compaction/compaction_begin/enable
echo 1 > events/compaction/compaction_end/enable
echo 1 > events/compaction/compact_zone/enable
echo 1 > events/vmscan/mm_compaction_isolate_freepages/enable
echo 1 > tracing_on
# 查看trace
cat trace | head -100
# 关键事件:
# compaction_begin / compaction_end:记录一次完整compaction的耗时
# compact_zone:展示扫描范围和结果
7.4 实用诊断脚本
#!/bin/bash
# compaction_monitor.sh - 实时监控compaction活动
echo "=== $(date) Compaction Status ==="
echo "--- vmstat compact ct ---"
grep -i compact /proc/vmstat | head -12
echo ""
echo "--- Buddy Info (Normal zone) ---"
awk '/Normal/ {print "Free chunks by order (4KB units):"; print $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15}' /proc/buddyinfo
echo ""
echo "--- Fragmentation Score ---"
# 计算碎片评分:高阶空闲比例低=碎片严重
node0_normal=$(head -1 /proc/buddyinfo)
# 提取order 10+ 的空闲数
high_order=$(echo "$node0_normal" | awk '{for(i<=15;i>=12;i--) sum+=$i} END{print sum+0}')
total=$(echo "$node0_normal" | awk '{for(i=4;i<=15;i++) sum+=$i} END{print sum+0}')
if [ "$total" -gt 0 ]; then
ratio=$((high_order * 100 / total))
echo "High-order(16MB+) chunks: $high_order / Total: $total (${ratio}% of total chunks)"
if [ "$ratio" -lt 5 ]; then
echo "⚠️ SEVERE FRAGMENTATION DETECTED!"
elif [ "$ratio" -lt 20 ]; then
echo "⚡ Moderate fragmentation, monitor closely"
else
echo "✅ Fragmentation level acceptable"
fi
fi
echo ""
echo "--- kcompactd status ---"
for pid in $(pgrep kcompactd); do
echo "PID $pid: $(cat /proc/$pid/stat | awk '{print "CPU:", $14+$15, "State:", $3}')"
done
echo ""
echo "--- THP / Compaction interaction ---"
echo "thp_defrag: $(cat /sys/kernel/mm/transparent_hugepage/defrag 2>/dev/null | head -1)"
echo "thp_enabled: $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null | head -1)"
echo "compact_unevictable: $(cat /proc/sys/vm/compact_unevictable_allowed)"
八、调优参数与工程实践
8.1 核心sysctl参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| /proc/sys/vm/compact_memory | 0 | echo 1对所有zone执行一次compaction |
| /proc/sys/vm/compaction_proactiveness | 20-500 | 主动compaction激进度(5.18+) |
| /proc/sys/vm/compact_unevictable_allowed | 1 | 是否扫描不可驱逐页面 |
| /sys/kernel/mm/transparent_hugepage/defrag | always/defer | THP碎片处理策略 |
| /sys/kernel/mm/transparent_hugepage/khugepaged/max_pte_missed | THP kcompactd阈值 |
8.2 compactive_proactiveness调优(内核5.18+)
这是最关键的compaction调优参数,控制compaction的积极程度:
- 0:几乎不做主动compaction(仅在分配失败且其他方法无效时触发)
- 50:Linux 6.x 默认值,平衡模式
- 200:旧内核默认值,积极模式
- 500:极激进模式,kcompactd频繁工作
设置建议:
# 数据库/OLTP工作负载(需要THP,对时延敏感)
echo 200 > /proc/sys/vm/compaction_proactiveness
# 批处理/离线计算(不关心compaction开销)
echo 0 > /proc/sys/vm/compaction_proactiveness
# 通用Web/中间件
echo 50 > /proc/sys/vm/compaction_proactiveness
8.3 THP策略选择
场景 │ THP defrag策略 │ 原因
─────────────────────────────┼────────────────────┼──────────────────────
MySQL/PostgreSQL OLTP │ madvise │ 只在堆/缓冲池中启用THP
大数据Spark/Flink │ always │ 需要最大内存带宽
Redis/Memcached │ never │ 延迟敏感,不要compact开销
Java/JVM堆 │ madvise │ JVM使用madvise配THP
实时音视频处理 │ always+高proactive │ 必须保证大页分配成功
容器密度化环境 │ defer+madvise │ 避免compact拉高CPU
8.4 针对碎片不可逆场景的处理
当碎片已不可挽回时(compaction_success极少而大量fail):
# 1. 手动触发全量compaction
echo 1 > /proc/sys/vm/compact_memory
# 2. 如果compact_memory后仍然无效,考虑:
# - 检查是否有大量不可移动内核页(分析slabtop)
# - 调整swappiness加速swap释放(将匿名页转为可交换)
# - 终极方案:通过NUMA rebalance将内存从一个node迁移走(如果有多节点)
# - 对于PCIe BAR/DMA区域碎片,只能重启
# 3. 检查是否有内存泄漏导致分配失败但不释放
echo 3 > /proc/sys/vm/drop_caches # 先清理pagecache看是否能临时恢复
九、代码级深度解析:compact_zone 核心路径
以下是简化后的 compact_zone 主循环,揭示compaction的实际执行逻辑:
static enum compact_result compact_zone(struct zone *zone, struct compact_control *cc)
{
unsigned long start_pfn = zone->zone_start_pfn;
unsigned long end_pfn = zone_end_pfn(zone);
cc->migrate_pfn = start_pfn;
cc->free_pfn = end_pfn;
while (cc->migrate_pfn < end_pfn-1 || cc->free_pfn > start_pfn) {
// ---- 阶段1:收集空闲页面 ----
if (!cc->nr_freepages) {
switch (isolate_migratepages(cc)) {
case ISOLATE_SUCCESS: // 有足够可迁移页面
continue;
case ISOLATE_NONE: // 没有可迁移页面了
// 判断是否是碎片问题还是真的无页
goto checkcompact;
case ISOLATE_ABORT: // 需要重试(如正在回写)
ret = COMPACT_CONTINUE;
goto break_loop;
}
}
// ---- 阶段2:执行迁移 ----
ret = migrate_pages(&cc->migratepages, ...);
cc->nr_freepages = 0;
}
checkcompact:
// 尝试用收集到的空闲页面满足分配请求
ret = __compaction_suitable(zone, cc->order, ...);
return ret;
}
核心要点:
- 扫描方向是从zone两端向中间推进(free从底向上,migrate从顶向下)
- 每个循环只做有限迁移,避免单次compaction阻塞过久
- 如果两个扫描器相遇,说明当前区域无可迁移空间
十、工程最佳实践清单
- 监控而非忽视:将compact_stall和compact_fail纳入监控告警,这些是内存碎片恶化的先兆指标。
- 理解碎片成因:不同负载的碎片模式不同——数据库是大量不可回收内核页+匿名页交错;网络包处理是DMA映射页面碎片化。
- THP不是银弹:在延迟敏感场景中,
madvise模式 + 合适的compaction_proactiveness比always模式更稳定。 - kcompactd调优优先:如果kcompactd频繁唤醒但成功率低,首先检查并调高碎片阈值(/proc/sys/vm/extfrag_threshold),避免无意义的compact尝试。
- 大页预留要谨慎:hugepages=NNN静态保留会减少可用常规分配内存,间接加剧碎片。在需要THP的环境中使用transparent hugepage + compaction比静态hugepage更灵活。
- 避免compaction风暴:多GPU/FPGA场景,如果设备驱动持续分配大块DMA内存,会导致反复compact失败并产生大量stall。应将这类设备分配到独立NUMA节点。
- 内核版本差异:Linux 5.10引入
compaction_proactiveness,5.18重新设计了主动compaction逻辑,6.x进一步优化了async compaction。不同版本默认行为差异较大,升级需回归测试。 - 故障排查顺序:
1. /proc/vmstat → compact_stall过高? 2. /proc/buddyinfo → 高阶连续块为0? 3. /proc/slabinfo → 不可移动SLAB对象太多? 4. ftrace compaction事件 → 单次耗时和范围? 5. 检查设备驱动 /proc/iomem → DMA区域碎片化?
总结
Memory Compaction 是 Linux 物理内存管理系统中承上启下的关键模块——上接 Buddy System 的高阶分配需求,下启 THP/DMA/hugetlb 等大页分配场景。理解 compaction 不仅是解决碎片问题的钥匙,更是构建可预测低延迟系统的基础。
在生产环境中,判断 compaction 是否健康的标准不是"什么都不做",而是 stall/fail 的比值足够低,且 compaction 开销相对于页面迁移收益是值得的。通过碎片索引、proactiveness 调优和合理的 THP 策略组合,可以让绝大多数负载对碎片"免疫"。
推荐延伸阅读:
- 本系列前篇:《Linux内核伙伴系统(Buddy System)物理内存分配深度工程实战》
- 本系列前篇:《Linux内核Slab分配器(Slab Allocator)物理内存分配深度工程实战》
- 内核源码:mm/compact.c、mm/migrate.c、mm/page_alloc.c
- kernel doc: Documentation/admin-guide/mm/compaction.rst

发表评论 取消回复