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(可迁移),意味着该区域内的页面可以被安全迁移:
- 启动阶段:通过 early_init 确定 CMA 区域物理范围,内存被标记为 MIGRATE_MOVABLE,加入伙伴系统但标记为 CMA
- 正常使用阶段:CMA 区域可被用户进程或内核分配(仅限可迁移页面),通过 page->lru 链表管理
- 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 使用"水密"隔离策略:
- 扫描阶段:isolate_migratepages_scan() 从 zone->free_pfn 两侧扫描,收集 MIGRATE_MOVABLE 页面
- 迁移阶段:将收集到的页面迁移至 zone->migrate_scanner_pos 指向的区域
- 合并阶段:空闲页面向迁移方向移动,与已有空闲页面合并成高阶块
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 源码是掌握这些机制的最佳途径。

发表评论 取消回复