一、当 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 的创新设计。它存储 首次被扫描到 的匿名页面。这些页面尚未被确认稳定——因为如果某一页面虽然当前内容相同,但随后频繁变动,它就不值得合并(合并/拆分的开销得不偿失)。
不稳定树的工作原理:
- 扫描线程发现一个匿名页面,计算其 checksum
- 在不稳定树中查找是否有相同 checksum 的已有节点
- 如果找到 → 增加节点的
seqnr(序列号),表示"这个页面连续多次内容相同" - 如果未找到 → 创建新节点插入不稳定树
- 当某个节点的
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_scan | 100 | 每次扫描的页面数 |
sleep_millisecs | /sys/kernel/mm/ksm/sleep_millisecs | 20ms | 两次扫描间的休眠时间 |
merge_across_nodes | /sys/kernel/mm/ksm/merge_across_nodes | 1 | 是否跨 NUMA 节点合并 |
run | /sys/kernel/mm/ksm/run | 0 (关闭) | KSM 总开关 |
max_page_sharing | /sys/kernel/mm/ksm/max_page_sharing | 256 | 物理页面的最大共享者数 |
stable_node_chains_prune_millisecs | /sys/kernel/mm/ksm/stable_node_chains_prune_millisecs | 10 | 稳定链表修剪间隔 |
use_zero_pages | /sys/kernel/mm/ksm/use_zero_pages | 0 | 是否合并全零页 |
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 管理)
- 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 分裂时间差,探测其他虚拟机中是否存在特定内容的页面。
攻击原理:
- 攻击者 VM 准备一个包含"目标内容"的页面,通过
madvise(..., MADV_MERGABLE)标记为可合并 - 如果受害者 VM 中存在相同内容的页面 → KSM 会将两者合并 → 攻击者的页面变为只读共享状态
- 攻击者尝试写入该页面 → 如果触发 CoW 分裂 → 说明之前被合并(受害者有相同内容)
- 通过精确计时 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 物理内存都发挥最大价值。

发表评论 取消回复