一、当 100 台虚拟机运行着相同的操作系统

在一台物理服务器上运行 100 台 KVM 虚拟机,每台分配 4GB 内存——这意味着 400GB 的内存需求。但如果你仔细观察,就会发现一个惊人的事实:这些虚拟机中 大量内存页面是完全相同的。相同的操作系统内核代码、相同的 libc 库、相同的静态数据段、全零页面……它们在物理内存中重复存储了成百上千份。

KSM (Kernel Samepage Merging) 正是 Linux 内核为解决这个问题而生的去重机制。它能识别内容相同的物理页面,合并为一份共享副本,通过写时复制(CoW)保护写入者。在密集的虚拟化部署中,KSM 可以节省 30%~50% 甚至更多的内存。

但 KSM 的魅力不止于节省内存——它的内核实现是一个精巧的工程奇迹:两阶段红黑树哈希扫描、页面校验和比对、不稳定树的自适应进化、NUMA 拓扑感知合并。理解 KSM,你就能理解 Linux 内核如何在性能与正确性之间走钢丝。

二、KSM 核心数据结构:稳定树与不稳定树

KSM 的核心由两棵红黑树组成,它们分别承担不同的角色:

2.1 稳定树(Stable Tree)— 已确认的共享页面

稳定树存储 已经去重成功 的页面。每个节点对应一个唯一的页面内容(通过 checksum 哈希索引),节点内维护一个 kpagenode 结构,包含:

struct rmap_item {
    struct rmap_item *rmap_list;    // 哈希链表(处理哈希冲突)
    struct anon_vma *anon_vma;      // 虚拟地址空间
    struct mm_struct *mm;           // 所属进程
    unsigned long address;          // 虚拟地址
    unsigned int seqnr;             // 节点序列号
};

稳定树的关键属性:

  • 只读页面:稳定树中的页面被标记为只读,任何写入操作会触发 CoW 分裂
  • 多对一映射:同一个稳定节点可以被多个 rmap_item 引用(即多个虚拟页面指向同一物理页面)
  • kuPAGEID 唯一标识:每个节点通过 checksum 和内容双重验证确定唯一性

2.2 不稳定树(Unstable Tree)— 候选页面的"培育温室"

不稳定树是 KSM 的创新设计。它存储 首次被扫描到 的匿名页面。这些页面尚未被确认稳定——因为如果某一页面虽然当前内容相同,但随后频繁变动,它就不值得合并(合并/拆分的开销得不偿失)。

不稳定树的工作原理:

  1. 扫描线程发现一个匿名页面,计算其 checksum
  2. 在不稳定树中查找是否有相同 checksum 的已有节点
  3. 如果找到 → 增加节点的 seqnr(序列号),表示"这个页面连续多次内容相同"
  4. 如果未找到 → 创建新节点插入不稳定树
  5. 当某个节点的 seqnr 超过稳定化阈值 → 晋升到稳定树

这种"观察期"机制保证了:只有内容真正稳定的页面才会被合并,避免了频繁的 CoW 分裂。

2.3 页面校验与比对流程

KSM 对每个页面执行两层检查:

// 第一层:快速 checksum 筛选(基于 xor 的滚动哈希)
checksum = calculate_checksum(page_address);

// 第二层:精确字节比对(仅在 checksum 匹配时执行)
if (checksum == node->checksum) {
    // 使用内核加密 API 计算 SHA-256 强哈希做二次确认
    if (memcmp(page1, node_page, PAGE_SIZE) == 0) {
        // 确认内容相同,可以合并
    }
}

为了加速校验,Linux 5.x 引入了 使用 Crypto API 计算高强度哈希(可配置为 SHA-256 或简单的 xxhash),大幅降低了哈希冲突风险。

三、KSM 扫描算法:页面选择与节奏控制

KSM 的扫描不是盲目遍历所有内存,而是有策略地选择页面。

3.1 扫描参数调优

参数路径默认值含义
pages_to_scan/sys/kernel/mm/ksm/pages_to_scan100每次扫描的页面数
sleep_millisecs/sys/kernel/mm/ksm/sleep_millisecs20ms两次扫描间的休眠时间
merge_across_nodes/sys/kernel/mm/ksm/merge_across_nodes1是否跨 NUMA 节点合并
run/sys/kernel/mm/ksm/run0 (关闭)KSM 总开关
max_page_sharing/sys/kernel/mm/ksm/max_page_sharing256物理页面的最大共享者数
stable_node_chains_prune_millisecs/sys/kernel/mm/ksm/stable_node_chains_prune_millisecs10稳定链表修剪间隔
use_zero_pages/sys/kernel/mm/ksm/use_zero_pages0是否合并全零页

3.2 扫描策略与算法

KSM 扫描器在每个 mm_struct 的红黑树(按 VMA 地址排序)中遍历:每次从上次断点继续扫描 pages_to_scan 个页面。这种增量式扫描保证了:

  • 公平性:所有进程的 VMA 都会被轮流扫描
  • 低延迟:每次扫描量可控,不阻塞用户态进程
  • 适应性:不稳定树的"自愈"能力会自动过滤掉不稳定的页面

3.3 NUMA 感知合并

在 NUMA 系统中,跨节点访问内存的延迟可能是本地节点的 2~3 倍。KSM 的 merge_across_nodes 参数控制了是否允许跨 NUMA 节点合并:

  • merge_across_nodes=1(默认):允许跨节点合并,最大化内存节省
  • merge_across_nodes=0:只允许同一 NUMA 节点内合并,优先保证访问性能

对于 KVM 虚拟机,如果虚拟机的 vCPU 绑定在特定 NUMA 节点,建议设置为 0;如果追求最大密度,设置为 1。

四、用户态接口:madvise 与 KVM 集成

4.1 madvise 显式标记

用户态程序可以通过 madvise() 系统调用显式标记内存区域为"可合并":

#include <sys/mman.h>

// 标记一段匿名内存区域为 KSM 可合并
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
                 MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(ptr, size, MADV_MERGABLE);

// 取消标记(例如关键数据结构不希望被合并)
madvise(ptr, size, MADV_UNMERGABLE);

注意:MADV_MERGABLE 只标记页面 候选 参加 KSM 扫描,不保证最终一定会合并。实际合并取决于页面内容稳定性。

4.2 KVM 虚拟化自动集成

KVM 是 KSM 的最大受益者。KVM 在创建虚拟机时,会自动将虚拟机的 匿名内存区域 标记为 MADV_MERGABLE(通过 QEMU 的 -mem-merge on 参数控制,默认开启):

// QEMU 启动参数示例
qemu-system-x86_64 \
    -machine accel=kvm \
    -m 4G \
    -mem-prealloc \
    -object memory-backend-ram,id=mem0,size=4G,merge=on \
    ...

KVM 的 EPT(Extended Page Table)/ NPT(Nested Page Table)机制与 KSM 无缝配合:

    li>Guest 虚拟地址 → Guest 物理地址(由 Guest 页表管理) li>Guest 物理地址 → Host 物理地址(由 EPT/NPT 管理)
  1. KSM 操作的是 Host 物理页面,对 Guest 完全透明

这意味着:即使 Guest OS 不知道 KSM 的存在,Guest 内的相同操作系统页面也会在 Host 层面被去重。

五、KSM 与 THP 的"相爱相杀"

KSM 和 THP(Transparent Huge Pages)是一对需要谨慎平衡的机制:

5.1 冲突根源

// THP 优先模式(默认)
echo always > /sys/kernel/mm/transparent_hugepage/enabled

// KSM 会拆分 THP
// 当 KSM 发现一个 THP 大页中的 512 个 4K 子页中有可合并的候选时
// 它会将 THP 拆分为独立的 4K 页面

这就是所谓的 "KSM vs THP 拆分风暴":THP 拼命合并 4K 页面为 2M 大页(提升 TLB 命中率),而 KSM 又把 2M 大页拆回 4K 以便扫描去重——两者相互拆台。

5.2 生产环境平衡策略

推荐方案:根据工作负载类型选择优先级

# 虚拟化密度优先(KVM 场景)— KSM 优先
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo 1 > /sys/kernel/mm/ksm/run
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan    # 加大扫描量
echo 10 > /sys/kernel/mm/ksm/sleep_millisecs    # 加快扫描频率

# 数据库/大数据场景 — THP 优先
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo 0 > /sys/kernel/mm/ksm/run                  # 禁用 KSM

# 混合场景 — madvise + KSM
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo 1 > /sys/kernel/mm/ksm/run
# 让需要 THP 的程序(如 JVM、Redis)通过 madvise(..., MADV_HUGEPAGE) 主动申请

5.3 KSM 与 Huge Page 合并(Linux 6.x 实验性)

Linux 6.x 引入了对 2M 大页的 KSM 合并 实验性支持(CONFIG_KSM_HUGE_PAGES),允许 KSM 直接合并相同内容的 THP,避免拆分-重复合并的无效开销。这标志着 KSM 与 THP 从对立走向协同。

六、监控与可观测性

6.1 sysfs 监控接口

# 查看 KSM 全局状态
cat /sys/kernel/mm/ksm/run                    # 0=关闭, 1=开启
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         # 内容变化频繁被剔除的页面
cat /sys/kernel/mm/ksm/full_scans             # 完整扫描轮次
cat /sys/kernel/mm/ksm/stable_node_chains     # 稳定树链表数
cat /sys/kernel/mm/ksm/stable_node_dups       # 稳定树重复项数

# 计算节省的内存量
echo "内存节省: $(($(cat /sys/kernel/mm/ksm/pages_sharing) - $(cat /sys/kernel/mm/ksm/pages_shared))) 页 × 4KB"
echo "即: $(( ($(cat /sys/kernel/mm/ksm/pages_sharing) - $(cat /sys/kernel/mm/ksm/pages_shared)) * 4 / 1024 )) MB"

6.2 Prometheus + Node Exporter 集成

# node-exporter 默认提供 node_ksm_* 指标
# Prometheus 查询示例

# KSM 共享页面数
node_ksm_pages_shared

# KSM 节省的内存 (Bytes)
(node_ksm_pages_sharing - node_ksm_pages_shared) * 4096

# KSM 合并效率(共享率 = pages_shared / pages_sharing)
node_ksm_pages_shared / node_ksm_pages_sharing * 100

6.3 告警规则建议

# KSM 高 CPU 开销告警(ksmd 线程占用过高)
- alert: KSMHighCPU
  expr: rate(process_cpu_seconds_total{job="ksmd"}[5m]) > 0.3
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "KSM 扫描消耗过多 CPU"
    
# KSM 内存节省率过低告警
- alert: KSMLowSavings
  expr: (node_ksm_pages_sharing - node_ksm_pages_shared) * 4096 < 104857600
  for: 30m
  labels:
    severity: info
  annotations:
    summary: "KSM 节省内存不足 100MB,可考虑关闭以节省 CPU"

七、安全攻防:侧信道攻击与防御

7.1 KSmear 攻击:通过 KSM 探测跨 VM 内存

2016 年,安全研究者发现了 KSmear 攻击:恶意虚拟机利用 KSM 的 CoW 分裂时间差,探测其他虚拟机中是否存在特定内容的页面。

攻击原理:

  1. 攻击者 VM 准备一个包含"目标内容"的页面,通过 madvise(..., MADV_MERGABLE) 标记为可合并
  2. 如果受害者 VM 中存在相同内容的页面 → KSM 会将两者合并 → 攻击者的页面变为只读共享状态
  3. 攻击者尝试写入该页面 → 如果触发 CoW 分裂 → 说明之前被合并(受害者有相同内容)
  4. 通过精确计时 CoW 分裂耗时,即可推断受害者 VM 中是否存在特定的内存内容

2019 年,KSmear 攻击被证实可以跨虚拟机窃取加密密钥信息,成为虚拟化安全的重大隐患。

7.2 防御机制

Linux 内核引入了多项针对 KSM 侧信道的防御:

// 1. KSM 最大共享者限制
echo 2 > /sys/kernel/mm/ksm/max_page_sharing

// 2. KSM 安全模式(拒绝跨进程合并)
echo 0 > /sys/kernel/mm/ksm/run
# 改为仅在单个进程内合并(通过 prctl 控制)

// 3. 使用 KVM 的 KSM 隔离
# libvirt 配置:<memoryBacking><nosharepages/></memoryBacking>
# 禁止该虚拟机的内存被 KSM 共享

生产建议:在多租户公有云中,如果不同租户运行在同一物理机上,强烈建议为敏感虚拟机设置 <nosharepages/>,或为 KSM 设置 max_page_sharing=2 以限制合并深度。

八、容器场景中的 KSM

8.1 Docker / containerd 支持

容器运行时不直接暴露 KSM 接口,但可以通过以下方式控制:

# Docker 层面:通过 --memory 和 --kernel-memory 间接影响
# 容器内匿名页面可通过 madvise 标记

# 在 Dockerfile 中标记堆内存为可合并
ENV MALLOC_TRIM_THRESHOLD_=131072
ENV MALLOC_MMAP_THRESHOLD_=131072

# 在应用代码中显式调用 madvise
import ctypes
libc = ctypes.CDLL('libc.so.6')
MADV_MERGABLE = 12
libc.madvise(ptr, size, MADV_MERGABLE)

8.2 Kubernetes 环境

在 K8s 中,KSM 通常由基础设施管理员在节点层面配置:

# DaemonSet 方式配置 KSM
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: ksm-configurator
spec:
  template:
    spec:
      hostPID: true
      containers:
      - name: ksm-config
        image: busybox
        command: ["/bin/sh", "-c"]
        args:
          - echo 1 > /sys/kernel/mm/ksm/run;
            echo 500 > /sys/kernel/mm/ksm/pages_to_scan;
            echo 20 > /sys/kernel/mm/ksm/sleep_millisecs;
            sleep infinity
        securityContext:
          privileged: true

8.3 KSM 在容器密度优化中的效果

在一个典型的容器密度场景中(同一基础镜像的多个 Pod),KSM 可以快速发现并合并相同的操作系统页面。根据实际测试:

  • 相同基础镜像的容器:首分钟节省 30%~40% 内存
  • 运行相同服务的容器:半小时后节省可达 50%~60%
  • 不同基础镜像的容器:节省率较低(约 10%~20%)

九、内核实现源码精读

9.1 ksmd 内核线程

// mm/ksm.c — ksmd 主循环
static int ksmd(void *nothing)
{
    set_freezable();
    set_user_nice(current, 5);  // 低优先级,不抢占业务

    while (!kthread_should_stop()) {
        wait_event_freezable(ksm_thread_wait,
            ksm_run_should_run() || kthread_should_stop());
        
        if (kthread_should_stop())
            break;

        // 执行一轮扫描
        ksm_do_scan(ksm_thread_pages_to_scan);

        // 休眠等待下一轮
        wait_event_timeout(ksm_thread_wait,
            ksm_run_should_run(),
            msecs_to_jiffies(ksm_thread_sleep_millisecs));
    }
    return 0;
}

9.2 页面合并核心逻辑

// mm/ksm.c — 稳定树中的页面合并
static page ksmd(struct page *page, struct rmap_item *rmap_item)
{
    struct page *kpage;
    struct rmap_item *tree_rmap;
    int err;

    // 1. 获取该页面的稳定树节点
    kpage = stable_tree_search(page);
    
    if (kpage == page) {
        // 页面已有的稳定副本就是自己,跳过
        goto out;
    }
    
    if (kpage) {
        // 2. 找到稳定副本 — 执行 CoW 迁移
        err = try_to_merge_with_ksm_page(rmap_item, page, kpage);
        if (!err) {
            // 合并成功:将虚拟页面重定向到共享的稳定页面
            // 原物理页面被释放
            break_cow(rmap_item, page);
        }
    } else {
        // 3. 未找到稳定副本 — 放入不稳定树观察
        err = unstable_tree_insert(page, checksum, rmap_item);
    }

out:
    return kpage;
}

十、生产环境实战案例分析

10.1 VPS 托管平台:密度翻倍

一个基于 KVM 的 VPS 服务商,物理节点 256GB 内存,原每台 VM 分配 2GB,最多运行 100 台。启用 KSM 后:

  • pages_sharing:峰值达到 45,000 页(约 176MB)
  • pages_shared:稳定在 35,000 页(约 137MB)
  • 净节省:约 176MB - (45000-35000)×4KB = 176MB - 390MB → 实际增享约 214MB
  • 部署密度:从 100 台增至 140 台,收入提升 40%

10.2 CI/CD 构建集群:容器密度优化

在每台 512GB 内存的构建节点上运行 200 个编译容器:

  • 基础镜像相同:全部使用 ubuntu:22.04
  • KSM 配置:pages_to_scan=1000, sleep_millisecs=10, use_zero_pages=1
  • 效果:30 分钟后节省约 8GB 内存(每个容器基础镜像部分约 40MB 被去重)
  • 额外可用密度:可多运行 20% 的容器

10.3 Redis 集群节点:反面教材

在 Redis 集群物理机上开启 KSM 的反面案例:

  • 问题:Redis 数据变化频繁,大量页面被判为 volatile(不稳定)
  • 副作用:ksmd 线程消耗 15% CPU,但合并收益极低(< 1%)
  • 教训:数据库、缓存类应用不建议开启 KSM

十一、未来展望:KSM 演进方向

11.1 持久化 KSM(Persistent KSM)

当前 KSM 的合并结果不会持久化——重启后需要重新扫描。未来可能引入"预训练"机制:将已知的稳定页面签名库加载到 KSM 稳定树中,在虚拟机启动时立即匹配,加快收敛速度。

11.2 与 CXL(Compute Express Link)协同

CXL 内存扩展设备普及后,KSM 将承担更关键的角色:将去重后的页面优先留在本地 DRAM,而被驱逐到 CXL 内存的页面则可以更激进地合并。这种分层内存 + KSM 的组合将最大化昂贵的本地 DRAM 利用率。

11.3 KSM-eBPF 联动

通过 eBPF 程序实现更智能的 KSM 候选选择:利用 eBPF 跟踪内存写入模式,动态调整 KSM 扫描策略,优先扫描写入频率低的页面区域,实现更精准的去重。

十二、快速上手:十分钟配置指南

#!/bin/bash
# KSM 快速配置脚本(适用于 KVM 虚拟化宿主)

# 1. 启用 KSM
echo 1 > /sys/kernel/mm/ksm/run

# 2. 调优扫描参数(默认 100 页/20ms,建议加大)
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan    # 每次扫描 1000 页
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs    # 每 20ms 扫描一轮

# 3. 启用全零页合并(安全且收益明显)
echo 1 > /sys/kernel/mm/ksm/use_zero_pages

# 4. NUMA 策略(根据场景选择)
echo 1 > /sys/kernel/mm/ksm/merge_across_nodes  # 跨节点合并(追求密度)
# echo 0 > /sys/kernel/mm/ksm/merge_across_nodes # 节点内合并(追求延迟)

# 5. THP 策略
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled

# 6. 监控节省效果
watch -n 1 'echo "Shared: $(cat /sys/kernel/mm/ksm/pages_shared)  Saving: $(( ($(cat /sys/kernel/mm/ksm/pages_sharing) - $(cat /sys/kernel/mm/ksm/pages_shared)) * 4 )) KB"'

# 7. 开机自启(systemd)
cat > /etc/systemd/system/ksm.service << EOF
[Unit]
Description=Kernel Samepage Merging
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/bash -c "echo 1 > /sys/kernel/mm/ksm/run; echo 1000 > /sys/kernel/mm/ksm/pages_to_scan; echo 20 > /sys/kernel/mm/ksm/sleep_millisecs; echo 1 > /sys/kernel/mm/ksm/use_zero_pages"
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF
systemctl enable ksm

十三、核心要点回顾

  • KSM = 内存去重:将内容相同的匿名页面合并为一份共享副本,通过 CoW 保护写入
  • 两阶段树:不稳定树(观察候选)→ 稳定树(确认合并),保证合并稳定性
  • 主要场景:KVM 虚拟化密度优化、容器部署密度提升
  • 不适合:数据库/缓存等频繁写入场景、安全敏感的多租户环境
  • THP 互斥:两者相互影响,建议虚拟化场景用 madvise 模式 + KSM
  • 安全威胁:KSmear 侧信道攻击,多租户环境需谨慎配置
  • 监控指标:pages_shared、pages_sharing、full_scans 是核心观测点

KSM 是 Linux 内核中最被低估的机制之一。在云原生时代,当"密度"就是收益时,掌握 KSM 的原理与调优,让你的每一 GB 物理内存都发挥最大价值。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论