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_memory0echo 1对所有zone执行一次compaction
/proc/sys/vm/compaction_proactiveness20-500主动compaction激进度(5.18+)
/proc/sys/vm/compact_unevictable_allowed1是否扫描不可驱逐页面
/sys/kernel/mm/transparent_hugepage/defragalways/deferTHP碎片处理策略
/sys/kernel/mm/transparent_hugepage/khugepaged/max_pte_missedTHP 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阻塞过久
  • 如果两个扫描器相遇,说明当前区域无可迁移空间

十、工程最佳实践清单

  1. 监控而非忽视:将compact_stall和compact_fail纳入监控告警,这些是内存碎片恶化的先兆指标。
  2. 理解碎片成因:不同负载的碎片模式不同——数据库是大量不可回收内核页+匿名页交错;网络包处理是DMA映射页面碎片化。
  3. THP不是银弹:在延迟敏感场景中,madvise模式 + 合适的compaction_proactiveness比always模式更稳定。
  4. kcompactd调优优先:如果kcompactd频繁唤醒但成功率低,首先检查并调高碎片阈值(/proc/sys/vm/extfrag_threshold),避免无意义的compact尝试。
  5. 大页预留要谨慎:hugepages=NNN静态保留会减少可用常规分配内存,间接加剧碎片。在需要THP的环境中使用transparent hugepage + compaction比静态hugepage更灵活。
  6. 避免compaction风暴:多GPU/FPGA场景,如果设备驱动持续分配大块DMA内存,会导致反复compact失败并产生大量stall。应将这类设备分配到独立NUMA节点。
  7. 内核版本差异:Linux 5.10引入compaction_proactiveness,5.18重新设计了主动compaction逻辑,6.x进一步优化了async compaction。不同版本默认行为差异较大,升级需回归测试。
  8. 故障排查顺序:
    
       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
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部