3、KSM合并流程详解:从页面扫描、内容比较、写保护到页面合并的完整生命周期
KSM(Kernel Same-page Merging)的合并流程并非一次性完成,而是由内核后台线程 ksmd 周期性驱动的多阶段流水线。理解这一生命周期是调优扫描效率与合并命中率的前提。整个流程可归纳为四个核心阶段:页面扫描、内容比较、写保护与页面合并。
核心要点: KSM 的合并操作对用户态进程完全透明。合并后的页面在物理上只保留一份副本,但所有参与进程的页表项均指向该副本,且被标记为“写保护”(read-only)。任何进程尝试写入时,会触发缺页异常(COW),内核为其分配独立副本。
3.1 整体生命周期总览
下图展示了 KSM 从候选页面选定到最终合并的完整状态机流转。每个矩形代表一个关键阶段,箭头表示数据流与状态迁移方向。
3.2 阶段一:页面扫描(Page Scanning)
ksmd 线程按照 pages_to_scan 参数设定的批次大小,从当前进程的 内存区域描述符(mm_struct) 中选取候选页面。扫描策略分为两种:
| 扫描模式 | 触发条件 | 行为描述 |
|---|---|---|
| 全量扫描 | 首次合并或稳定树重建 | 遍历所有匿名页面,跳过已合并页与锁定页 |
| 增量扫描 | 正常周期性运行 | 仅扫描 pages_to_scan 指定数量的新页面 |
调优提示: 扫描速度由
/sys/kernel/mm/ksm/pages_to_scan 控制。增大该值可加速收敛,但会占用更多 CPU。建议在业务低峰期调高,高峰期调低。
扫描阶段的关键数据结构是 稳定树(stable tree) 与 不稳定树(unstable tree)。稳定树保存已合并的页面内容(以页面内容的 hash 值为 key),不稳定树保存正在比较的候选页面。
3.3 阶段二:内容比较(Content Comparison)
对于每个扫描到的候选页面,KSM 执行两级比较:
- 哈希预检: 计算候选页面的 32-bit 校验和(checksum),与上次扫描值对比。若未变化,则跳过后续比较(减少 CPU 开销)。
- 全量 memcmp: 若校验和发生变化,则进入红黑树查找。先在稳定树中按内容 hash 查找完全匹配的页面;若未命中,则在不稳定树中查找。
比较过程涉及两次 memcmp 调用:
/* 内核源码示意:mm/ksm.c */
static int cmp_and_merge_page(struct page *page, struct rmap_item *rmap_item)
{
/* 1. 计算校验和 */
unsigned int checksum = calc_checksum(page);
if (checksum == rmap_item->oldchecksum) {
/* 页面内容未变,跳过 */
return 0;
}
rmap_item->oldchecksum = checksum;
/* 2. 在稳定树中查找 */
struct page *kpage = stable_tree_search(page);
if (kpage) {
/* 命中稳定树,直接合并 */
try_to_merge_with_ksm_page(rmap_item, page, kpage);
return 1;
}
/* 3. 在不稳定树中查找 */
kpage = unstable_tree_search_insert(rmap_item, page, &tree_rmap_item);
if (kpage) {
/* 命中不稳定树,尝试合并 */
try_to_merge_two_pages(rmap_item, page, tree_rmap_item, kpage);
return 1;
}
return 0;
}
性能陷阱: 不稳定树中的页面在每次扫描周期结束后会被清空。如果两个相同内容的页面在不同周期被扫描,它们永远不会在不稳定树中相遇,导致合并机会丢失。增大
pages_to_scan 或降低 sleep_millisecs 可缓解此问题。
3.4 阶段三:写保护(Write Protection)
当 KSM 判定两个页面内容完全一致后,并不会立即释放物理页。首先执行 写保护 操作:
- 将参与合并的所有进程的页表项中的 写权限位(RW) 清除,置为只读。
- 在页表项中设置 KSM 标记位(如
PTE_SPECIAL或架构相关的软件位),标识该页已被 KSM 管理。 - 更新反向映射(reverse mapping)结构,记录所有映射此页面的
rmap_item。
写保护完成后,任何进程对该页面的写入都会触发 缺页异常(page fault),进入 COW 处理流程。
3.5 阶段四:页面合并(Page Merging)
写保护完成后,KSM 执行最终的物理页面合并:
- 选定保留页: 选择其中一个物理页作为“合并页”(ksm page),通常为稳定树中已存在的页面。
- 更新页表: 将所有参与进程的页表项指向该保留页的物理地址。
- 释放冗余页: 调用
put_page()递减其他冗余物理页的引用计数。当引用计数降为 0 时,内核回收该页。 - 更新稳定树: 将合并后的页面插入稳定树,并记录所有映射它的
rmap_item。
| 合并类型 | 触发场景 | 性能开销 |
|---|---|---|
| 稳定树合并 | 候选页面与稳定树中已有页面匹配 | 低(仅一次 memcmp + 页表更新) |
| 不稳定树合并 | 两个候选页面在同一扫描周期内匹配 | 中(两次 memcmp + 树插入 + 页表更新) |
| 全量合并 | 稳定树重建或首次合并 | 高(全量扫描 + 多次比较) |
3.6 合并后的生命周期管理
合并后的页面并非永久固定。以下事件会导致合并页分裂:
- 写时复制(COW): 任何进程对合并页执行写入操作,内核立即分配新物理页,复制内容,并更新该进程的页表为可读写。
- 页面迁移(Page Migration): 内存规整(compaction)或 NUMA 平衡可能移动合并页,KSM 需要更新稳定树中的指针。
- 进程退出: 当最后一个映射合并页的进程退出时,KSM 从稳定树中移除该页并释放物理内存。
工程实践建议: 监控
/sys/kernel/mm/ksm/pages_sharing 与 /sys/kernel/mm/ksm/pages_shared 的比值。若 pages_sharing / pages_shared 接近 1,说明合并效率低(每个合并页只被少量进程共享);比值越大,内存节省效果越好。
3.7 完整流程时序图
以下时序图展示了 ksmd 线程在一次扫描周期内的完整操作序列:
3.8 关键数据结构关系
KSM 合并流程依赖三个核心数据结构协同工作:
| 数据结构 | 存储位置 | 作用 | 生命周期 |
|---|---|---|---|
| stable_tree | 红黑树(全局) | 保存已合并页面的内容 hash → page 映射 | 持久存在,随合并/分裂更新 |
| unstable_tree | 红黑树(全局) | 保存当前周期候选页面的内容 hash → page 映射 | 每个扫描周期结束时清空 |
| rmap_item | 每个进程的匿名页反向映射链 | 记录单个页面与进程的映射关系及校验和 | 随进程内存映射变化而创建/销毁 |
源码路径提示: 上述数据结构的定义与操作集中在
mm/ksm.c 文件中。稳定树查找函数 stable_tree_search() 与不稳定树插入函数 unstable_tree_search_insert() 是性能关键路径,优化时需重点关注。