1. KSM 架构总览:内存去重的艺术
KSM(Kernel Samepage Merging)是 Linux 内核的一项内存管理功能,通过合并相同内容的物理页面来减少内存占用。自 2.6.32 版本合入主线以来,KSM 已成为 KVM 虚拟化环境中内存超分配(Memory Overcommit)的核心技术。
其核心工作流程:
- 候选页面注册:用户通过
madvise(addr, len, MADV_MERGEABLE)将内存区域标记为可合并 - 后台扫描线程:
ksmd内核线程周期性扫描注册的内存页 - 页面校验:计算页面内容的校验和(checksum),快速排除不同页面
- 逐字节比较:对候选对进行精确的逐字节比较,确认完全相同
- 合并操作:将多个相同页面合并为一个只读页面(写时复制保护)
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 不是免费的,主要开销包括:
- CPU 开销:ksmd 内核线程周期性扫描,消耗 CPU 周期计算校验和、遍历红黑树、执行逐字节比较
- 延迟抖动:每次页面写入触发 COW 分裂时,需要分配新页面并复制内容
- 搜索复杂度:不稳定树中的每次搜索是 O(log n),但每次扫描可能涉及大量候选页面
关键权衡:内存节省 vs CPU 消耗,生产环境中需要根据工作负载特征调整参数。
6. Sysfs 调优参数实战
KSM 的所有运行时参数位于 /sys/kernel/mm/ksm/:p>
| 参数 | 默认值 | 说明 |
|---|---|---|
run | 0 | 0=禁用,1=启用,2=停止并卸载 |
pages_to_scan | 100 | 每次 ksmd 睡眠间隔内扫描的页面数 |
sleep_millisecs | 20 | 两次扫描之间的睡眠时间(毫秒) |
merge_across_nodes | 1 | 是否跨 NUMA 节点合并 |
max_page_sharing | code>256 | 单个页面允许的最大共享者数量 |
shared_page | code>- | 内存中当前被 KSM 共享的页面数 |
pages_sharing | - | 通过合并节省的页面总数 |
pages_shared | - | 当前被多个 VMA 指向的共享页面数 |
pages_unshared | - | 扫描后确认不重复的页面数 |
pages_volatile | - | 变化太快无法合并的页面数 |
full_scans | - | ksmd 完成的完整扫描轮数 |
stable_node_chains | - | 稳定树中的链数量 |
stable_node_chains_prune_millisecs | 2000 | 稳定树过期间隔 |
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+:增强变体的对比
| 特性 | 主线 KSM | UKSM | KSM+ |
|---|---|---|---|
| 上游合并 | ✅ 主线 | ❌ 需补丁 | ❌ 需补丁 |
| 校验和算法 | jhash | sha256 + crc32 | crc32 |
| 扫描速度 | 页面级(4K) | 字节级 | 4K + 提示级 |
| CPU 开销 | 低 | 高 | 中 |
| 内存节省 | 中 | 高(高重复场景) | 中高 |
| THP 兼容 | 默认跳过 | 尝试拆分后合并 | 可选策略 |
| NUMA 感知 | merge_across_nodes | 节点感知 | 本地优先 |
UKSM 在极端高密度场景(Gnome 桌面、空闲虚拟机集群)有更多合并机会,但因 CPU 开销高、安全性验证不足,未被主线合入。
10. 生产环境最佳实践
- 场景选择:
- ✅ KVM 虚拟机同构高密度部署
- ✅ 开发/测试 CI 环境
- ⚠️ 数据库/缓存服务(页面变化频繁,收益低)
- ❌ 安全敏感环境(KSM 可能引发侧信道攻击)
- 参数调优步骤:
- 启用后观察 24 小时的
pages_sharing / pages_to_scan比例 - 如果
pages_volatile / pages_shared比值过高(>5),降低扫描强度 - 监控宿主机 CPU 使用率增量,KSM 开销不应超过 5%
- 启用后观察 24 小时的
- 安全考量:
- KSM 引发时序侧信道(通过 COW 延迟推断其他 VM 的内存内容)
- 高安全环境应禁用 KSM:
echo 2 > /sys/kernel/mm/ksm/run - CVE-2020-28588 等漏洞曾通过 KSM 跨租户泄露信息
- 与内存气球配合:
- KSM 合并 + Balloon 回收 = 最大化超分配能力
- libvirt 配置同时启用 balloon 和 KSM 可达 1:3 超分配比
- 但需注意气球回收后页面又重新分配时的时间窗口一致性
总结
KSM 是 Linux 内核内存管理中一个优雅而实用的子系统。通过"校验和快速筛选 → 精确字节比较 → 红黑树组织 → 只读合并 → COW 分裂"的设计,KSM 在 KVM 虚拟化环境中实现了显著的内存节省。
理解 KSM 的工作机理不仅有助于优化虚拟化方案的内存效率,也能加深对 Linux 内核页面管理、红黑树数据结构、以及内存去重领域的工程理解。
在生产环境中,KSM 的调优需要在节省内存和 CPU 开销之间找到平衡点,结合具体工作负载特征进行场景化配置,并持续监控关键指标。

发表评论 取消回复