3、KSM合并流程详解:从页面扫描、内容比较、写保护到页面合并的完整生命周期

KSM(Kernel Same-page Merging)的合并流程并非一次性完成,而是由内核后台线程 ksmd 周期性驱动的多阶段流水线。理解这一生命周期是调优扫描效率与合并命中率的前提。整个流程可归纳为四个核心阶段:页面扫描、内容比较、写保护与页面合并。

核心要点: KSM 的合并操作对用户态进程完全透明。合并后的页面在物理上只保留一份副本,但所有参与进程的页表项均指向该副本,且被标记为“写保护”(read-only)。任何进程尝试写入时,会触发缺页异常(COW),内核为其分配独立副本。

3.1 整体生命周期总览

下图展示了 KSM 从候选页面选定到最终合并的完整状态机流转。每个矩形代表一个关键阶段,箭头表示数据流与状态迁移方向。

KSM 页面合并生命周期总览 阶段1:页面扫描 ksmd 遍历进程内存 阶段2:内容比较 红黑树查找 + memcmp 阶段3:写保护 页表置为只读 阶段4:页面合并 释放冗余物理页 扫描队列循环(稳定树回退) 关键:写保护触发 COW 写入时自动分裂合并页

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 执行两级比较:

  1. 哈希预检: 计算候选页面的 32-bit 校验和(checksum),与上次扫描值对比。若未变化,则跳过后续比较(减少 CPU 开销)。
  2. 全量 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 处理流程。

写保护与 COW 触发流程 进程A 页表 可读写 (RW) 进程B 页表 可读写 (RW) 物理页 内容: ABC 进程A 页表 只读 (RO) + KSM 进程B 页表 只读 (RO) + KSM 物理页 内容: ABC 进程A 写入 → COW 分裂

3.5 阶段四:页面合并(Page Merging)

写保护完成后,KSM 执行最终的物理页面合并:

  1. 选定保留页: 选择其中一个物理页作为“合并页”(ksm page),通常为稳定树中已存在的页面。
  2. 更新页表: 将所有参与进程的页表项指向该保留页的物理地址。
  3. 释放冗余页: 调用 put_page() 递减其他冗余物理页的引用计数。当引用计数降为 0 时,内核回收该页。
  4. 更新稳定树: 将合并后的页面插入稳定树,并记录所有映射它的 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 线程在一次扫描周期内的完整操作序列:

ksmd 单次扫描周期时序 时间 1. 选择候选页面 2. 计算校验和 3. 稳定树查找 命中 → 直接合并 4. 不稳定树查找 命中 → 合并并插入稳定树 5. 插入不稳定树 6. 等待下一周期 关键参数影响 pages_to_scan:批次大小 sleep_millisecs:周期间隔 max_page_sharing:共享上限

3.8 关键数据结构关系

KSM 合并流程依赖三个核心数据结构协同工作:

数据结构 存储位置 作用 生命周期
stable_tree 红黑树(全局) 保存已合并页面的内容 hash → page 映射 持久存在,随合并/分裂更新
unstable_tree 红黑树(全局) 保存当前周期候选页面的内容 hash → page 映射 每个扫描周期结束时清空
rmap_item 每个进程的匿名页反向映射链 记录单个页面与进程的映射关系及校验和 随进程内存映射变化而创建/销毁
源码路径提示: 上述数据结构的定义与操作集中在 mm/ksm.c 文件中。稳定树查找函数 stable_tree_search() 与不稳定树插入函数 unstable_tree_search_insert() 是性能关键路径,优化时需重点关注。