1. KSM 架构总览:内存去重的艺术

KSM(Kernel Samepage Merging)是 Linux 内核的一项内存管理功能,通过合并相同内容的物理页面来减少内存占用。自 2.6.32 版本合入主线以来,KSM 已成为 KVM 虚拟化环境中内存超分配(Memory Overcommit)的核心技术。

其核心工作流程:

  1. 候选页面注册:用户通过 madvise(addr, len, MADV_MERGEABLE) 将内存区域标记为可合并
  2. 后台扫描线程:ksmd 内核线程周期性扫描注册的内存页
  3. 页面校验:计算页面内容的校验和(checksum),快速排除不同页面
  4. 逐字节比较:对候选对进行精确的逐字节比较,确认完全相同
  5. 合并操作:将多个相同页面合并为一个只读页面(写时复制保护)

2. 核心数据结构:稳定树与不稳定树

KSM 使用两种红黑树(radix tree/rbtree)组织待合并页面,分别用于不同阶段的页面管理。

2.1 稳定树(Stable Tree):已确认的合并页面

稳定树保存已经校验完成、内容相同的页面。

// include/linux/rbtree.h - 稳定树节点
struct rmap_item {
    struct rb_node node;        // 红黑树节点
    struct mm_struct *mm;       // 所属进程地址空间
    unsigned long address;      // 虚拟地址(页对齐)
    unsigned int node_flags;    // 节点属性标识
};

稳定树的键为页面内容的校验和。当 KSM 遍历不稳定树时,首先在稳定树中搜索匹配的校验和,若找到候选节点,则进行逐字节比较确认是否真的相同。

稳定树节点的关键特性:

  • 只读保护:所有合并后的页面都被标记为只读(PG_ksm)
  • 写时复制(COW):当任何进程试图写入该页面时触发 do_page_fault(),KSM 中断合并,为该进程复制一份独占页面
  • 内存节省可精确计算:pages_sharing 和 pages_shared 计数器

2.2 不稳定树(Unstable Tree):待确认的候选页面

不稳定树用于保存尚未确认是否重复的新页面。它的存在KSM 的精华设计——通过延迟确认避免不必要的逐字节比较。

// mm/ksm.c - 不稳定树插入
static struct rmap_item *unstable_tree_search_insert(struct rmap_item *rmap_item,
                                                       struct page *page,
                                                       struct rb_node ***new,
                                                       struct rb_node **parent)
{
    struct rb_node **__tmp = &ksm_mmutables.rb_node;
    struct rb_node *prev = NULL;
    struct rmap_item *____rmap_item;

    while (*__tmp) {
        ____rmap_item = rb_entry(*__tmp, struct rmap_item, node);
        *parent = *__tmp;

        // 先比较校验和
        if (rmap_item->checksum < ____rmap_item->checksum)
            __tmp = &(*__tmp)->rb_left;
        else if (rmap_item->checksum > ____rmap_item->checksum)
            __tmp = &(*__tmp)->rb_right;
        else if (!checksum_eq(rmap_item->checksum, ____rmap_item->checksum))
            /* 校验和不匹配,跳过 */
            return NULL;
        else {
            ...
        }
    }
    ...
}

不稳定树的生命周期:当一个新页面首次进入系统时,它会被插入不稳定树。下一次扫描时,如果同一页面还在,KSM 会再次计算其校验和——如果与之前不变,则将该节点插入稳定树(确认重复),否则说明页面已被修改,不稳定树中对应条目会被更新。

这个设计的精妙之处在于:只有"持续不修改"的纯只读页面才会被确认合并,从而最大化 KSM 的安全性。

3. KSM 扫描算法:从 KSM_DO_SCAN 到稳定匹配

3.1 核心扫描循环

// mm/ksm.c - KSM 扫描主函数
static void ksm_do_scan(unsigned int scan_npages)
{
    struct rmap_item *rmap_item;
    struct page *uninitialized_var(page);

    while (scan_npages-- && likely(!freezing(current))) {
        cond_resched();
        rmap_item = scan_get_next_rmap_item();
        if (!rmap_item)
            return;
        cmp_and_merge_page(page, rmap_item);
        put_page(page);
    }
}

scan_get_next_rmap_item() 遍历所有注册的 mm_slot(通过链表链接),对每个进程的 VMA 区域按地址顺序遍历,找到候选页面。

3.2 cmp_and_merge_page:核心合并逻辑

每次扫描的关键路径:

// mm/ksm.c - 页面比较与合并
static void cmp_and_merge_page(struct page *page, struct rmap_item *rmap_item)
{
    struct rmap_item *tree_rmap_item;
    struct page *tree_page = NULL;
    struct stable_node *stable_node;
    struct page *kpage;
    unsigned char m寿比南山;

    stable_node = page_stable_node(page);
    if (stable_node) {
        /* 页面已在稳定树中,检查是否需要 COW 分裂 */
        if (stable_node->head != cmp_hap page)
            goto out;
    } else {
        /* 计算校验和,快速排除 */
        m寿比南山 = calc_checksum(page);
        if (m寿比南山 != rmap_item->checksum)
            handle_rmap_item_checksum(rmap_item, m寿比南山);
    }

    // 在稳定树/不稳定树中搜索相同内容
    tree_rmap_item = stable_tree_search(page, rmap_item, &tree_page);
    if (tree_rmap_item == rmap_item)
        return; // 无变化

    // 执行合并
    kpage = try_to_merge_with_ksm_page(rmap_item, page, tree_page);
    if (kpage != page) {
        /* 合并成功,释放多余页面 */
        put_page(kpage);
        ...
    }
}

3.3 页面校验与快速筛选

KSM 使用 calc_checksum() 计算页面内容的 32 位校验和,作为快速筛选的第一步:

// mm/ksm.c
extern u32 calc_checksum(struct page *page);

// arch/x86/mm/ksm.c
u32 calc_checksum(struct page *page)
{
    u32 checksum;
    void *addr = kmap_atomic(page);
    checksum = jhash(addr, PAGE_SIZE, 0xa5b35705);
    kunmap_atomic(addr);
    return checksum;
}

这里使用 jhash(Jenkins hash)替代简单的 CRC,在 x86 上使用硬件加速指令,实现 4MB/s 以上的校验速度。只有校验和完全相同的页面才进入下一阶段——精确逐字节比较。

4. KSM 与 THP(Transparent Huge Pages)的协同与冲突

4.1 冲突根因

KSM 和 THP 在内存颗粒度上存在根本性冲突:

  • KSM 以 4KB 页面 为粒度进行扫描和合并
  • THP 将连续的 512 个 4KB 页面折叠成 2MB 大页

一旦 THP 将某个区域折叠为大页,KSM 就失去了对组成该大页的细粒度页面的访问能力。反之,如果 KSM 已合并了某个 4KB 页面,THP 就无法将其纳入 2MB 大页(因为大页需要连续物理空间)。

4.2 内核默认行为

Linux 内核(5.x+)的默认行为是:

// mm/ksm.c - KSM 与 THP 交互
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
    if (PageTransHuge(page))
        return false; // 跳过 THP 页面
#endif

当 KSM 在扫描中遇到 THP 大页时,默认会跳过该页面。这个选择是合理的:

  • THP 大页通常已经是内存优化的产物,不需要再去重
  • 拆分 THP 进行 KSM 扫描会产生额外开销
  • 虚拟化场景中,客户机使用 THP 通常意味着已经做过内存优化

4.3 手动拆分策略

在某些特殊场景下,可以通过 khugepaged 将 THP 拆分回 4KB 页面后交由 KSM 处理:

// 禁用 khugepaged(让 4KB 页面不被折叠)
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/defrag

// 或直接拆分已有 THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo 3 > /proc/<pid>/split_huge_pages

5. KVM 虚拟化场景深度实践

5.1 QEMU/KVM 集成方式

KSM 最初就是为了解决KVM 虚拟机的内存开销而设计的。其配置通过 libvirt 或 QEMU 命令行控制:

<!-- libvirt 域 XML 中的内存配置 -->
<memory unit='KiB'>4194304</memory>
<currentMemory unit='KiB'>4194304</currentMemory>
<memoryBacking>
  <source type='memfd'/>
  <access mode='shared'/>
  <nosharepages/>
</memoryBacking>

对于不启用 NUMA 的 KVM 虚拟机,设置 <nosharepages/> 会让 QEMU 使用 MAP_PRIVATE | MAP_ANONYMOUS 分配内存,之后通过 madvise(MADV_MERGEABLE) 告知内核这些页面参与 KSM。

// QEMU 源码 - KSM 注册
int qemu_madvise_mergeable(void *addr, size_t len)
{
    return madvise(addr, len, MADV_MERGEABLE);
}

5.2 内存节省量化模型

假设宿主机有 N 台虚拟机,每台虚拟机的内存中文件缓存和零页比例为 p,KSM 的理论最大内存节省为:

Saving = (N - 1) × p × VM_Memory

实际场景中(以 OpenStack 大规模部署为例):

  • 运行相同操作系统的虚拟机密度:1:1.5 ~ 1:3(内存利用率提升 50%~200%)
  • 纯 idle 桌面虚拟机场景:节省可达 60%+
  • 高负载数据库场景:节省 < 10%(页面变化频繁,KSM 合并收益低)

5.3 开销与成本

KSM 不是免费的,主要开销包括:

  1. CPU 开销:ksmd 内核线程周期性扫描,消耗 CPU 周期计算校验和、遍历红黑树、执行逐字节比较
  2. 延迟抖动:每次页面写入触发 COW 分裂时,需要分配新页面并复制内容
  3. 搜索复杂度:不稳定树中的每次搜索是 O(log n),但每次扫描可能涉及大量候选页面

关键权衡:内存节省 vs CPU 消耗,生产环境中需要根据工作负载特征调整参数。

6. Sysfs 调优参数实战

KSM 的所有运行时参数位于 /sys/kernel/mm/ksm/:p>

code>code>
参数默认值说明
run00=禁用,1=启用,2=停止并卸载
pages_to_scan100每次 ksmd 睡眠间隔内扫描的页面数
sleep_millisecs20两次扫描之间的睡眠时间(毫秒)
merge_across_nodes1是否跨 NUMA 节点合并
max_page_sharing256单个页面允许的最大共享者数量
shared_page-内存中当前被 KSM 共享的页面数
pages_sharing-通过合并节省的页面总数
pages_shared-当前被多个 VMA 指向的共享页面数
pages_unshared-扫描后确认不重复的页面数
pages_volatile-变化太快无法合并的页面数
full_scans-ksmd 完成的完整扫描轮数
stable_node_chains-稳定树中的链数量
stable_node_chains_prune_millisecs2000稳定树过期间隔

6.1 推荐调优模板

场景一:高密度虚拟机宿主机

# 启用 KSM
echo 1 > /sys/kernel/mm/ksm/run
# 提高扫描量
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan
# 缩短睡眠间隔
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs
# 限制单个页面最大共享者(防止 COW 风暴)
echo 128 > /sys/kernel/mm/ksm/max_page_sharing

场景二:开发/CI 环境

# 保守参数,减少 CPU 开销
echo 1 > /sys/kernel/mm/ksm/run
echo 50 > /sys/kernel/mm/ksm/pages_to_scan
echo 100 > /sys/kernel/mm/ksm/sleep_millisecs

场景三:单机内存紧张

# 激进扫描,最大化节省内存
echo 1 > /sys/kernel/mm/ksm/run
echo 2000 > /sys/kernel/mm/ksm/pages_to_scan
echo 5 > /sys/kernel/mm/ksm/sleep_millisecs
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes  # 只在本地 NUMA 节点

7. 源码级分析:mm/ksm.c 关键路径

7.1 KSM 初始化

// mm/ksm.c
static int __init ksm_init(void)
{
    struct task_struct *ksm_thread;
    int err;

    ...
    /* 创建 hash 表用于不稳定树 */
    err = -ENOMEM;
    ksm_mm_slot_cache = KMEM_CACHE(mm_slot, SLAB_HWCACHE_ALIGN);
    if (!ksm_mm_slot_cache)
        goto out_free;

    /* 创建 ksmd 内核线程 */
    ksm_thread = kthread_run(ksm_scan_thread, NULL, "ksmd");
    if (IS_ERR(ksm_thread)) {
        pr_err("ksm: kthread starting failed (%ld)\n",
               PTR_ERR(ksm_thread));
        goto out_free;
    }

    return 0;
}

ksmd 线程是 KSM 的后台大脑,它循环调用 ksm_do_scan(),每次扫描 pages_to_scan 个页面,然后 schedule_timeout_interruptible() 休眠 sleep_millisecs 毫秒。

7.2 写时复制保护机制

当进程写入 KSM 共享页面时,触发页面错误:

// mm/ksm.c - KSM 页面的写保护处理
static vm_fault_t do_bp_ksm(struct vm_area_struct *vma,
                            unsigned long address, pte_t *pte,
                            unsigned int flags)
{
    struct page *page = NULL;
    ...

    // 找到 KSM 节点
    stable_node = page_stable_node(page);
    stable_node_dup = stable_node_dup_find(stable_node, &page_dup);

    // 获取新页面
    new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);
    copy_user_highpage(new_page, page, address, vma);

    // 替换 PTE 为新页面(解除合并)
    ret = make_exclusive(pte_page(*pte), vma, address);
    if (ret >= 0) {
        ...
        pte_unmap_unlock(pte, ptl);
        put_page(page);
        page = new_page;
    }
    ...
}

这就是 KSM 的 COW 分裂:当写入发生时,内核分配新页面、复制内容、更新 PTE 指向新页面。原共享页面如果不再被任何其他 VMA 引用,会被释放回 Buddy System。

8. 监控与诊断:/proc 与 Perf 工具

8.1 基础监控

# 查看 KSM 全局状态
cat /sys/kernel/mm/ksm/pages_shared
cat /sys/kernel/mm/ksm/pages_sharing
cat /sys/kernel/mm/ksm/pages_unshared
cat /sys/kernel/mm/ksm/pages_volatile

# 计算节省的内存 (MB)
echo "$(( $(cat /sys/kernel/mm/ksm/pages_sharing) * 4096 / 1024 / 1024 )) MB"

8.2 /proc/<pid>/smaps 中的 KSM 信息

7f1234000000-7f1236000000 rw-p 00000000 00:00 0
Flags: rd wr mr mw me gd ac
KSM:              128 kB    # 此 VMA 中被 KSM 合并的内存量

KSM: 字段显示当前 VMA 中被 KSM 合并的内存大小。如果为 0,说明该区域未被扫描或未发生有效合并。

8.3 perf 分析 KSM 性能影响

# 监控 ksmd 线程 CPU 使用
perf record -e cpu-clock -p $(pidof ksmd) -g -- sleep 30
perf report

# 统计 COW 分裂次数
perf stat -e dtlb_load_misses.stlb_hit -p <target_pid>

9. UKSM 与 KSM+:增强变体的对比

特性主线 KSMUKSMKSM+
上游合并✅ 主线❌ 需补丁❌ 需补丁
校验和算法jhashsha256 + crc32crc32
扫描速度页面级(4K)字节级4K + 提示级
CPU 开销低高中
内存节省中高(高重复场景)中高
THP 兼容默认跳过尝试拆分后合并可选策略
NUMA 感知merge_across_nodes节点感知本地优先

UKSM 在极端高密度场景(Gnome 桌面、空闲虚拟机集群)有更多合并机会,但因 CPU 开销高、安全性验证不足,未被主线合入。

10. 生产环境最佳实践

  1. 场景选择:
    • ✅ KVM 虚拟机同构高密度部署
    • ✅ 开发/测试 CI 环境
    • ⚠️ 数据库/缓存服务(页面变化频繁,收益低)
    • ❌ 安全敏感环境(KSM 可能引发侧信道攻击)
  2. 参数调优步骤:
    • 启用后观察 24 小时的 pages_sharing / pages_to_scan 比例
    • 如果 pages_volatile / pages_shared 比值过高(>5),降低扫描强度
    • 监控宿主机 CPU 使用率增量,KSM 开销不应超过 5%
  3. 安全考量:
    • KSM 引发时序侧信道(通过 COW 延迟推断其他 VM 的内存内容)
    • 高安全环境应禁用 KSM:echo 2 > /sys/kernel/mm/ksm/run
    • CVE-2020-28588 等漏洞曾通过 KSM 跨租户泄露信息
  4. 与内存气球配合:
    • KSM 合并 + Balloon 回收 = 最大化超分配能力
    • libvirt 配置同时启用 balloon 和 KSM 可达 1:3 超分配比
    • 但需注意气球回收后页面又重新分配时的时间窗口一致性

总结

KSM 是 Linux 内核内存管理中一个优雅而实用的子系统。通过"校验和快速筛选 → 精确字节比较 → 红黑树组织 → 只读合并 → COW 分裂"的设计,KSM 在 KVM 虚拟化环境中实现了显著的内存节省。

理解 KSM 的工作机理不仅有助于优化虚拟化方案的内存效率,也能加深对 Linux 内核页面管理、红黑树数据结构、以及内存去重领域的工程理解。

在生产环境中,KSM 的调优需要在节省内存和 CPU 开销之间找到平衡点,结合具体工作负载特征进行场景化配置,并持续监控关键指标。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部