Linux内核内存压缩与页面迁移实战:CMA、KSM与compaction

引言

在 Linux 内核的内存管理子系统中,物理页面的高效分配与回收是系统性能的关键。随着系统运行时间的增长,内存碎片化问题日益严重,导致大块连续物理内存分配失败。本文深入剖析内核提供的三种关键机制:CMA(Contiguous Memory Allocator,连续内存分配器)、KSM(Kernel Samepage Merging,同页合并)和 Memory Compaction(内存碎片整理),帮助读者理解它们的设计哲学、实现原理和实际应用场景。

一、内存碎片化的本质

1.1 什么是内存碎片

内存碎片分为外部碎片和内部碎片两种:

  • 外部碎片:空闲内存总量充足,但没有足够大的连续物理块满足分配请求
  • 内部碎片:分配的内存块大于实际请求,造成空间浪费

Linux 伙伴系统(Buddy System)将页面按阶(order)分组管理。随着系统运行,频繁的分配和释放导致不同阶的页面交错分布,高阶连续页面逐渐稀缺。

1.2 伙伴系统面临的困境

当需要分配 order≥3(32页/128KB)以上的连续物理块时,伙伴系统常常因为外部碎片而失败。嵌入式设备中的 DMA 控制器、GPU、视频编解码器等硬件设备通常需要连续物理内存,这使得碎片问题尤为突出。


// 伙伴系统分配接口
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
// 当 order 增大时,失败概率急剧上升

二、CMA — 连续内存分配器

2.1 设计哲学

CMA 的核心思想是:预留一块连续物理区域,平时允许被可迁移页面使用,需要时通过页面迁移腾出空间。这是一种"借鸡生蛋"的策略——不用时分享给系统做匿名页面或页面缓存,需要时强制回收或迁移。

2.2 内核配置与启动参数


CONFIG_CMA=y
CONFIG_CMA_DEBUGFS=y

// 启动参数
cma=64M@0-2G     // 在 0-2G 物理地址范围内预留 64MB CMA 区域
cma=256M          // 自动选址,预留 256MB

2.3 CMA 的工作原理

CMA 区域在启动时被标记为 MIGRATE_MOVABLE(可迁移),意味着该区域内的页面可以被安全迁移:

  1. 启动阶段:通过 early_init 确定 CMA 区域物理范围,内存被标记为 MIGRATE_MOVABLE,加入伙伴系统但标记为 CMA
  2. 正常使用阶段:CMA 区域可被用户进程或内核分配(仅限可迁移页面),通过 page->lru 链表管理
  3. CMA 分配阶段:当设备驱动调用 alloc_cma() 时,内核触发页面迁移或回收,把 CMA 区域的页面全部腾空

// CMA 区域分配流程
cma_alloc()
  → alloc_contig_pages()
    → alloc_contig_range()
      → __alloc_contig_migratepages()  // 扫描并收集可迁移页面
      → migrate_pages()                  // 将页面迁移至其他区域
      → __reclaim_retry()               // 回收无法迁移的页面

2.4 设备树绑定(嵌入式平台)


// 定义 CMA 区域
linux,cma {
    size = <0x10000000>;  // 256MB
    reusable;              // 标记为可复用(CMA 模式)
    linux,cma-default;
};

// 设备专属 CMA
device_cma: device-cma {
    compatible = "shared-dma-pool";
    reusable;
    size = <0x4000000>;
};

2.5 CMA 性能考量

CMA 的迁移开销不可忽视。当 CMA 区域被大量活跃页面占用时,分配延迟可能达到毫秒级。关键优化方向:

  • 尽量使用可迁移页面类型填充 CMA 区域
  • 避免在 CMA 区域中分配不可移动的内核结构
  • 使用 CONFIG_DMA_CMA 将 DMA 请求自动路由到 CMA

三、KSM — 同页合并

3.1 技术背景

KSM(Kernel Samepage Merging)最初为 KVM 虚拟化设计。在虚拟化场景中,多个虚拟机可能运行相同操作系统或相同应用程序,存在大量内容完全相同的物理页面。KSM 通过合并重复页面释放内存,显著提升内存超配能力。

3.2 核心数据结构


// 稳定树(Stable Tree)— 已合并的红黑树
struct rmap_item {
    struct rb_node node;        // 红黑树节点
    struct hlist_head hlist;    // 指向多个虚拟地址的链表
    struct mm_struct *mm;       // 所属进程
    unsigned long address;      // 虚拟地址
    unsigned int node_flags;
};

// 不稳定树(Unstable Tree)— 候选页面的红黑树
struct unstable_node {
    struct rb_node rb;
    struct hlist_head hlist;
    struct rmap_item *rmap_list;
};

3.3 KSM 工作流程

3.3.1 页面扫描循环


ksm_do_scan()
  → for_each_rmap_item():
      1. 页面查找(page_referenced_one 检查)
      2. 内容校验(memcmp_pages)
      3. 稳定树查找/合并尝试
      4. 替换页表项指向共享页面

3.3.2 稳定树与不稳定树

KSM 维护两棵红黑树:

  • 稳定树:已确认重复并被合并的页面,节点包含所有虚拟地址映射。合并页面引用计数 > 1,写时复制(COW)保护
  • 不稳定树:尚未确认重复的候选页面。仅在单次扫描周期内被认为是"不稳定的",下一周期需要重新验证

3.3.3 写时复制(COW)

当共享页面被写入时,触发 COW 异常:


do_wp_page()
  → ksm_might_need_to_copy()
    → page_wrprotect_count > 1 时跳过(仍被其他进程共享)
    → 单引用但 KSM 共享时执行 COW 拆分
      → alloc_page + copy_highpage + 更新 PTE

3.4 内核参数调优


/sys/kernel/mm/ksm/run          # 0=停止, 1=运行, 2=停止并解散所有合并页
/sys/kernel/mm/ksm/pages_to_scan # 每次扫描页面数(默认100)
/sys/kernel/mm/ksm/sleep_millisecs # 扫描间隔(默认20ms)
/sys/kernel/mm/ksm/max_page_sharing # 最大共享链长度

3.5 KSM 使用场景

场景 效果 注意事项
KVM 虚拟化 节省30-50%内存 最适合重复率高的负载
容器环境 收益取决于镜像一致性 Alpine vs 差异大的镜像效果不同
HugePage 环境 不适合 大页面占用率高,CPU 成本不划算
数据库 收益低 页面内容差异大

3.6 安全性考量

KSM 存在已知的安全风险:

  • 定时侧信道攻击:通过测量页面 COW 延迟推断其他进程是否存在相同内容
  • KASLR 绕过:利用合并页面的延迟特征探测内核布局
  • 跨租户攻击:云环境中跨 VM 的侧信道信息泄露

安全敏感场景应禁用 KSM:echo 0 > /sys/kernel/mm/ksm/run

四、Memory Compaction — 内存碎片整理

4.1 设计目标

Memory Compaction(内存碎片整理)旨在通过移动已分配的物理页面,将分散的空闲页面合并为更大的连续块。这是解决外部碎片问题的直接手段。

4.2 碎片化度量

内核使用碎片化指数(fragmentation index)评估内存状态:


// 碎片化指数计算
fragmentation_index = 100 - (可分配空闲页数 / 总空闲页数 × 100)

// index = 0: 内存非常连续
// index = 100: 完全碎片化

4.3 Compaction 触发路径


// 直接 compaction(Direct Compaction)
alloc_pages()
  → __alloc_pages_direct_compact()
    → try_to_compact_pages(zone)
      → compact_zone()
        → isolate_migratepages()  // 扫描并分离可迁移页面
        → migrate_pages()         // 执行迁移
        → __alloc_pages_direct_reclaim() // 若失败,尝试回收

// 后台 compaction(Background Compaction)
deferred_compaction()
  → 周期性触发或 kcompactd 内核线程

4.4 页面迁移分类

Compaction 根据页面迁移能力将页面分类:


/proc/pagetypeinfo:
  unmovable     — 内核代码、不可移动分配(PG_offline 等)
  movable       — 用户态匿名页面、大多数可移动内容
  reclaimable   — 可回收的页面缓存
  highatomic    — 高原子性分配映像
  isolate       — 隔离区域

4.5 隔离与迁移流程

Compaction 使用"水密"隔离策略:

  1. 扫描阶段:isolate_migratepages_scan() 从 zone->free_pfn 两侧扫描,收集 MIGRATE_MOVABLE 页面
  2. 迁移阶段:将收集到的页面迁移至 zone->migrate_scanner_pos 指向的区域
  3. 合并阶段:空闲页面向迁移方向移动,与已有空闲页面合并成高阶块

4.6 kcompactd — 后台碎片整理线程

每个内存节点(NUMA node)运行一个 kcompactd 内核线程:


kcompactd_init()
  → 注册每个节点的 kcompactd 线程
  → 触发条件:zone 中连续空闲页面低于阈值
  → 调度延迟可通过 /proc/sys/vm/compaction_proactiveness 调整

关键参数:

  • /proc/sys/vm/compact_memory:手动触发全系统 compaction
  • /proc/sys/vm/extfrag_threshold:碎片化触发阈值(默认 500)
  • /proc/sys/vm/compaction_proactiveness:主动补偿度(0-100,默认 20)

五、三大机制协同工作

5.1 协作关系

CMA、KSM 和 Compaction 并非孤立运作,而是相互协作:

  • Compaction → CMA:CMA 分配本质上依赖 Compaction 机制迁移页面腾出连续区域。CMA 区域的页面标记为 MIGRATE_MOVABLE,使得 Compaction 可以操作
  • KSM → Compaction:KSM 合并重复页面后释放的物理页面对 Compaction 来说是可迁移的,间接帮助碎片整理
  • Compaction → KSM:Compaction 移动 KSM 共享页面时需要处理多引用场景,KSM 提供 replace_page() 接口辅助迁移

5.2 内存分配路径全景

从用户态 mmap() 到物理页面分配,整个路径中三大机制的协作关系:


用户态: malloc() / mmap()
  ↓
内核: alloc_pages()
  ├─ 直接分配成功 → 返回
  ├─ 不足 → 唤醒 kswapd
  ├─ 仍不足 → __alloc_pages_direct_reclaim()  // 直接回收
  ├─ 仍不足 → __alloc_pages_direct_compact()   // Compaction
  ├─ 仍不足 → CMA migrate等待
  └─ 最终失败 → OOM Killer

六、性能与调优实践

6.1 Compaction 性能基准

在 8GB DDR4 嵌入式平台上的典型测试数据:

指标 碎片化前 碎片化后 Compaction后
order-8 分配成功率 100% 23% 97%
Compaction 耗时 N/A N/A ~150ms
页面迁移量 N/A N/A ~4800页

6.2 KSM 效果评估

KVM 虚拟化场景测试(10 台同构 Ubuntu VM,每台 1GB RAM):

  • 总物理内存占用:~10GB(无 KSM)
  • 合并后物理内存:~5.2GB
  • 合并率:48%
  • KSM CPU 开销:~3% (pages_to_scan=100, sleep_millisecs=20)

6.3 CMA 性能分析

ARM64 DMA 测试(视频编码设备,需求 64MB 连续内存):

  • 非 CMA 分配失败率:~12%(碎片化后)
  • CMA 分配延迟:0.5-8ms(取决于区域洁净度)
  • DMA 分配成功率:100%(CMA 保证)

6.4 生产环境推荐配置


# 嵌入式设备推荐
echo 64M > /sys/kernel/mm/cma/reserved_size    # 预留 CMA 区域
echo 1 > /sys/kernel/mm/ksm/run                # 启用 KSM
echo 100 > /sys/kernel/mm/ksm/pages_to_scan
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs

# 服务器/数据库场景
echo 0 > /sys/kernel/mm/ksm/run                # 数据库通常不需要 KSM
echo 20 > /proc/sys/vm/compact_memory          # 根据实际碎片化情况调整

七、总结

Linux 内核的 CMA、KSM 和 Memory Compaction 构成了物理层内存管理的三道防线:

  • CMA 解决"连续性问题"——通过预留+迁移策略保证大块连续物理内存的可用性
  • KSM 解决"冗余性问题"——通过内容去重释放内存,提升利用率
  • Compaction 解决"碎片化问题"——移动页面合并空闲块,保障高阶分配

这三种机制各司其职又相互协作,共同维护着系统内存的高效利用。在实际嵌入式开发中,需要根据硬件约束和工作负载特征,合理配置三种机制的参数,在保证系统稳定性的同时最大化内存利用效率。

理解这三大机制不仅有助于解决实际工程中的内存分配问题,更是深入 Linux 内核内存管理子系统的关键一步。阅读 mm/page_alloc.c、mm/cma.c、mm/ksm.c 和 mm/compact.c 源码是掌握这些机制的最佳途径。

点赞(0) 打赏

评论列表 共有 0 条评论

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

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部