源码版本:基于 Linux mainline(分析时间:2026-03) 主要文件:
mm/ksm.c、include/linux/ksm.h、mm/rmap.c、mm/memory.c
- KSM 设计背景与动机
- 核心数据结构
- 双红黑树设计:稳定树与不稳定树
- KSM 扫描流程
- 页面合并机制
- COW 触发与分裂
- EWMA 自动调速机制
- KSM 与 NUMA
- 侧信道攻击防护
- KSM 统计指标详解
- 进程级 KSM:MADV_MERGEABLE 与 prctl
- KSM 与 THP 的交互
- 反向映射(rmap)集成
- 内存迁移与热插拔支持
- sysfs 接口完整参考
- 性能分析与调优建议
- 关键代码路径总结
KSM(Kernel Samepage Merging)是 Linux 内核在 2.6.32 版本(2009 年)引入的内存去重机制,由 Red Hat 的 Izik Eidus、Andrea Arcangeli、Chris Wright 和 Hugh Dickins 编写。
mm/ksm.c:1-14(文件头注释):
// SPDX-License-Identifier: GPL-2.0-only
/*
* Memory merging support.
*
* This code enables dynamic sharing of identical pages found in different
* memory areas, even if they are not shared by fork()
*
* Copyright (C) 2008-2009 Red Hat, Inc.
* Authors:
* Izik Eidus
* Andrea Arcangeli
* Chris Wright
* Hugh Dickins
*/
KSM 的核心动机来自虚拟化场景。在一台物理主机上运行多个 KVM 虚拟机时,每个 guest 都有完整的 Linux 内核映像、libc、系统守护进程等。这些相同的代码和数据页面在物理内存中存在大量冗余副本:
典型 KVM 主机内存布局(8 个相同 Ubuntu guest):
物理内存(256 GB)
┌──────────────────────────────────────────────────────┐
│ Guest VM 1 │ Guest VM 2 │ ... │ Guest VM 8 │
│ 32 GB 各 │ 32 GB 各 │ │ 32 GB 各 │
│ │ │ │ │
│ Linux 内核代码(约 20 MB * 8 = 160 MB 冗余) │
│ /lib/libc.so(约 2 MB * 8 = 16 MB 冗余) │
│ /usr/bin/* 程序(大量相同) │
│ 零页(大量未使用内存) │
└──────────────────────────────────────────────────────┘
KSM 合并后:相同内容页只保留 1 份物理页
→ 节省 60-80 GB 物理内存(具体取决于工作负载)
与 fork() 的 COW 机制不同,KSM 不要求进程有父子关系,也不要求地址空间有任何关联。这使得它能够跨越完全独立的进程(包括不同 guest 的 QEMU 进程)发现并合并相同页面。
KSM、zswap 和 zram 是三种不同维度的内存压缩/节省技术:
内存优化技术对比:
技术 作用层面 原理 副作用
──────────────────────────────────────────────────────────
KSM 物理页去重 相同内容页共享 COW 延迟,扫描 CPU
zswap 压缩换出 压缩 swap 页 CPU 压缩/解压开销
zram 压缩 RAM 设备 内存中模拟块设备 同上
HugePages 减少 TLB 压力 2MB/1GB 大页 碎片,不能 KSM 直接合并
互补关系:
KSM 消除重复 → 减少总物理用量
zswap 压缩 → 延迟换出,提高 swap 效率
二者可同时启用,针对不同内存压力场景
KSM 优先处理热内存中的重复页(写保护后共享),而 zswap 处理被换出的冷页(压缩存储)。在内存极度紧张时,KSM 的收益可能降低(很多页已被换出),此时 zswap 更有效。
KSM 本身有运行开销,需要仔细权衡:
收益方:
- 每节省一个 4KB 页面,释放 4KB 物理内存(可用于页缓存、新分配等)
- 减少 TLB miss(多进程指向同一物理帧,I-TLB 复用率更高)
- 对 NUMA 场景可以减少跨节点访问(合并到本地 NUMA 节点)
成本方:
ksmd线程持续扫描,消耗 CPU(通常 1-5%,可通过 advisor 控制)- 每个候选页的
ksm_rmap_item占用约 40 字节内存 - COW 触发时的额外缺页异常延迟(通常 10-100 微秒)
- 写保护操作需要 TLB 刷新(跨 CPU 的 TLB shootdown)
临界点估算(mm/ksm.c:3439-3444):
long ksm_process_profit(struct mm_struct *mm)
{
return (long)(mm->ksm_merging_pages + mm_ksm_zero_pages(mm)) * PAGE_SIZE -
mm->ksm_rmap_items * sizeof(struct ksm_rmap_item);
}当 general_profit(全局)或 ksm_process_profit()(进程级)为负数时,意味着 KSM 元数据的内存开销超过了合并收益,需要减少候选区域。
用户空间 内核空间
┌──────────────────────────────────────────┐
madvise(MADV_MERGEABLE) ──>│ ksm_madvise() vm_flags |= VM_MERGEABLE │
prctl(PR_SET_MEMORY_MERGE)─>│ ksm_enable_merge_any() │
│ __ksm_enter() → mm 加入 mm_slots 链表 │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ksmd 内核线程(nice=5) │ │
│ │ ksm_scan_thread() │ │
│ │ └─> ksm_do_scan(pages_to_scan) │ │
│ │ └─> scan_get_next_rmap_item│ │
│ │ └─> cmp_and_merge_page│ │
│ └─────────────────────────────────────┘ │
│ │
│ ┌────────────┐ ┌──────────────────┐ │
│ │ 不稳定树 │ │ 稳定树 │ │
│ │(每轮清空) │ │ (永久保留) │ │
│ │ rb_root │ │ rb_root │ │
│ │(ksm_rmap_ │ │ (ksm_stable_node) │ │
│ │ item) │ │ │ │
│ └────────────┘ └──────────────────┘ │
└──────────────────────────────────────────┘
|
v
┌──────────────────────┐
│ KSM 共享页(只读) │
│ folio->mapping = │
│ stable_node | KSM │
└──────────────────────┘
↑ ↑ ↑
进程A 进程B 进程C (多个 PTE 指向同一物理页)
定义于 mm/ksm.c:201-221:
struct ksm_rmap_item {
struct ksm_rmap_item *rmap_list; /* mm_slot 单链表中的下一个 */
union {
struct anon_vma *anon_vma; /* 在稳定树中时:指向 anon_vma */
#ifdef CONFIG_NUMA
int nid; /* 在不稳定树中时:NUMA 节点 id */
#endif
};
struct mm_struct *mm; /* 所属内存空间 */
unsigned long address; /* 虚拟地址(低位用于标志位) */
unsigned int oldchecksum; /* 上次扫描的 xxhash 校验和 */
rmap_age_t age; /* 已扫描的迭代次数(u8 类型)*/
rmap_age_t remaining_skips; /* 智能扫描:剩余可跳过次数 */
union {
struct rb_node node; /* 在不稳定树中时:红黑树节点 */
struct { /* 在稳定树中时:挂入 stable_node */
struct ksm_stable_node *head;
struct hlist_node hlist;
};
};
};typedef 定义(mm/ksm.c:59):
typedef u8 rmap_age_t; /* age 是 8 位无符号整数,最大值 255 */地址字段的低位标志(mm/ksm.c:223-225):
#define SEQNR_MASK 0x0ff /* 低 8 位:存储不稳定树的序号(seqnr)*/
#define UNSTABLE_FLAG 0x100 /* 第 8 位:当前是不稳定树节点 */
#define STABLE_FLAG 0x200 /* 第 9 位:当前挂在稳定树 */address 字段同时承担双重职责:高位存储实际对齐地址(& PAGE_MASK),低 10 位编码状态标志。这是典型的内核位域复用技巧,在 4KB 页对齐(低 12 位为 0)保证下是安全的。
rmap_item 的状态转换:
新建
│
▼
[仅含虚拟地址]
address = vaddr
│
┌─────────┴──────────┐
│ checksum 连续两次相同│ checksum 变化
▼ ▼
插入不稳定树 丢弃(页内容不稳定)
address |= UNSTABLE_FLAG
address |= (seqnr & SEQNR_MASK)
│
│ 与另一页内容匹配
▼
合并进稳定树
address |= STABLE_FLAG
head -> ksm_stable_node
anon_vma 保持引用
内存布局:
ksm_rmap_item(约 40 字节,x86_64):
┌──────────────────┐ +0
│ rmap_list (8B) │ → 下一个 rmap_item(单链表)
├──────────────────┤ +8
│ anon_vma/nid(8B) │ 联合体:稳定时=anon_vma指针,不稳定时=NUMA nid
├──────────────────┤ +16
│ mm (8B) │ → mm_struct
├──────────────────┤ +24
│ address (8B) │ 虚拟地址 + 低位标志
├──────────────────┤ +32
│ oldchecksum (4B) │ xxhash32 校验和
├──────────────────┤ +36
│ age (1B) │ 扫描迭代次数
├──────────────────┤ +37
│remaining_skips1B │ 智能扫描剩余跳过次数
├──────────────────┤ +38
│[padding 2B] │
├──────────────────┤ +40
│ node/head+hlist │ 联合体(各 16-24B)
└──────────────────┘
定义于 mm/ksm.c:159-185:
struct ksm_stable_node {
union {
struct rb_node node; /* 作为稳定树节点时 */
struct { /* 作为迁移列表节点时 */
struct list_head *head;
struct {
struct hlist_node hlist_dup; /* 挂入 chain->hlist */
struct list_head list; /* 挂入 migrate_nodes */
};
};
};
struct hlist_head hlist; /* 指向所有引用此 KSM 页的 rmap_item */
union {
unsigned long kpfn; /* KSM 页的物理帧号 */
unsigned long chain_prune_time; /* chain 节点:上次垃圾回收时间 */
};
#define STABLE_NODE_CHAIN -1024
int rmap_hlist_len; /* rmap_item 数量,或 STABLE_NODE_CHAIN */
#ifdef CONFIG_NUMA
int nid; /* 所在 NUMA 节点 */
#endif
};关键字段解析:
-
kpfn:KSM 共享页的物理帧号(Page Frame Number)。稳定节点不持有页引用计数(folio refcount),通过"keyhole 引用"机制校验页是否仍然有效(ksm_get_folio(),mm/ksm.c:946)。 -
hlist:所有映射此 KSM 页的ksm_rmap_item以哈希链表形式挂载于此。 -
rmap_hlist_len:追踪 hlist 中的 rmap_item 数量。当其值为STABLE_NODE_CHAIN(-1024)时,表示这是 chain 节点。注释(mm/ksm.c:175-179)特意选择 -1024 而非 -1,以便可靠地检测下溢(内存损坏)。
两级链式结构(stable_node chain/dup):
当超过 ksm_max_page_sharing(默认 256)个映射引用同一内容的页时,内核不再使用单一 stable_node,而是建立 chain/dup 两级结构:
稳定树(rb_root)
└── ksm_stable_node [chain]
│ rmap_hlist_len = STABLE_NODE_CHAIN(-1024)
│ chain_prune_time = 上次 GC 时间
│
├─ [hlist_dup] → ksm_stable_node [dup1]
│ kpfn = 物理页 A
│ rmap_hlist_len = 256
│ hlist → rmap_item_1 → rmap_item_2 → ... (256个)
│
└─ [hlist_dup] → ksm_stable_node [dup2]
kpfn = 物理页 B(内容相同,但是不同物理帧)
rmap_hlist_len = 128
hlist → rmap_item_257 → ... (128个)
stable_node_chains_prune_millisecs(默认 2000ms)控制对 chain 中过期 dup 节点的垃圾回收频率。
/* mm/ksm.c:126-129 */
/**
* struct ksm_mm_slot - ksm information per mm that is being scanned
* @slot: hash lookup from mm to mm_slot
* @rmap_list: head for this mm_slot's singly-linked list of rmap_items
*/
struct ksm_mm_slot {
struct mm_slot slot; /* 含 hash 链表节点和 mm_node 链表节点 */
struct ksm_rmap_item *rmap_list; /* 此 mm 的 rmap_item 单链表头 */
};所有参与 KSM 的 mm 通过 mm_slots_hash 索引(mm/ksm.c:237-238):
#define MM_SLOTS_HASH_BITS 10
static DEFINE_HASHTABLE(mm_slots_hash, MM_SLOTS_HASH_BITS);哈希表大小为 2^10 = 1024,同时所有 mm_slot 形成以 ksm_mm_head 为哨兵节点的循环链表,供 ksmd 顺序扫描。
双重索引设计:
mm_slots_hash[hash(mm)]
│
├─ mm_slot_A ←──────────────────────────────────┐
├─ mm_slot_B │
└─ ... │
│
扫描链表(循环): │
ksm_mm_head ↔ mm_slot_A ↔ mm_slot_B ↔ mm_slot_C ──┘
→ 哈希表用于 O(1) 查找(按 mm 地址)
→ 链表用于顺序迭代(ksmd 扫描游标)
/* mm/ksm.c:140-145 */
/**
* struct ksm_scan - cursor for scanning
* @mm_slot: the current mm_slot we are scanning
* @address: the next address inside that to be scanned
* @rmap_list: link to the next rmap to be scanned in the rmap_list
* @seqnr: count of completed full scans (needed when removing unstable node)
*
* There is only the one ksm_scan instance of this cursor structure.
*/
struct ksm_scan {
struct ksm_mm_slot *mm_slot; /* 当前正在扫描的 mm_slot */
unsigned long address; /* 下一个要扫描的虚拟地址 */
struct ksm_rmap_item **rmap_list; /* rmap_item 链表的当前指针 */
unsigned long seqnr; /* 完整扫描轮次计数 */
};
static struct ksm_scan ksm_scan = {
.mm_slot = &ksm_mm_head,
};全局只有一个 ksm_scan 实例(mm/ksm.c:243-245),保存 ksmd 断点续扫的状态。seqnr 每完成一次完整扫描(遍历所有 mm 的所有 VMA)递增一次,用于识别不稳定树节点是否属于当前轮次。
/* mm/ksm.c:251-301(重要的全局变量)*/
#define DEFAULT_PAGES_TO_SCAN 100 /* 默认每批扫描页数 */
static unsigned long ksm_pages_scanned; /* 总扫描页数 */
static unsigned long ksm_pages_shared; /* 稳定树节点数(KSM 共享页数)*/
static unsigned long ksm_pages_sharing; /* 额外共享映射数 */
static unsigned long ksm_pages_unshared; /* 不稳定树节点数 */
static unsigned long ksm_rmap_items; /* 所有 rmap_item 总数 */
static unsigned long ksm_stable_node_chains; /* chain 节点数 */
static unsigned long ksm_stable_node_dups; /* dup 节点数 */
static unsigned int ksm_stable_node_chains_prune_millisecs = 2000;
static int ksm_max_page_sharing = 256;
static unsigned int ksm_thread_pages_to_scan = DEFAULT_PAGES_TO_SCAN;
static unsigned int ksm_thread_sleep_millisecs = 20;
static unsigned int zero_checksum __read_mostly; /* 零页的 xxhash 值 */
static bool ksm_use_zero_pages __read_mostly;
static bool ksm_smart_scan = true;
atomic_long_t ksm_zero_pages = ATOMIC_LONG_INIT(0); /* KSM 放置的零页总数 */
static unsigned long ksm_pages_skipped; /* smart_scan 跳过的页数 */运行状态标志(mm/ksm.c:478-482):
#define KSM_RUN_STOP 0 /* ksmd 停止扫描 */
#define KSM_RUN_MERGE 1 /* ksmd 正常合并 */
#define KSM_RUN_UNMERGE 2 /* 解除所有合并 */
#define KSM_RUN_OFFLINE 4 /* 内存热插拔下线中,暂停 */
static unsigned long ksm_run = KSM_RUN_STOP;/* mm/ksm.c:247-249 */
static struct kmem_cache *rmap_item_cache;
static struct kmem_cache *stable_node_cache;
static struct kmem_cache *mm_slot_cache;三种核心对象各有专属 slab 缓存,在 ksm_slab_init()(mm/ksm.c:490)中通过 KMEM_CACHE() 宏创建:
/* mm/ksm.c:490-498 */
static int __init ksm_slab_init(void)
{
rmap_item_cache = KMEM_CACHE(ksm_rmap_item, 0);
if (!rmap_item_cache)
goto out;
stable_node_cache = KMEM_CACHE(ksm_stable_node, 0);
if (!stable_node_cache)
goto out_free1;
...
}/* mm/ksm.c:228-231 */
static struct rb_root one_stable_tree[1] = { RB_ROOT };
static struct rb_root one_unstable_tree[1] = { RB_ROOT };
static struct rb_root *root_stable_tree = one_stable_tree;
static struct rb_root *root_unstable_tree = one_unstable_tree;默认情况下各只有一棵树(数组大小为 1)。当 merge_across_nodes=0 时,系统为每个 NUMA 节点维护独立的一对树(第 mm/ksm.c:3580-3598 行动态分配数组,大小为 nr_node_ids)。
注释(mm/ksm.c:117-119)说明了这一点:
* If the merge_across_nodes tunable is unset, then KSM maintains multiple
* stable trees and multiple unstable trees: one of each for each NUMA node.
代码注释(mm/ksm.c:107-111)对不稳定树使用红黑树作了深刻解释:
* 3) The unstable tree is a RedBlack Tree - so its balancing is based on the
* colors of the nodes and not on their contents, assuring that even when
* the tree gets "corrupted" it won't get out of balance, so scanning time
* remains the same (also, searching and inserting nodes in an rbtree uses
* the same algorithm, so we have no overhead when we flush and rebuild).
红黑树的平衡基于节点颜色(红/黑),而非节点键值(页内容)。即使树中某些节点的页内容在插入后发生了变化(树"腐化"),树的平衡性和遍历时间 O(log n) 依然有保证。同时搜索和插入算法相同,每轮清空重建无额外开销。
特性:
- 存储所有已合并的 KSM 页(写保护只读)
- 不会在每轮扫描后清空,永久保留直到 KSM 页被解除映射
- 节点为
ksm_stable_node,以页内容(memcmp_pages())作为比较键 - 每个节点通过
kpfn定位对应物理页,通过hlist维护所有映射
稳定树搜索(stable_tree_search(),mm/ksm.c:1825):
stable_tree_search(page)
│
├─ 遍历红黑树(O(log n))
│ ├─ ksm_get_folio(node, TRYLOCK) keyhole 机制获取并锁定 folio
│ │ ├─ folio->mapping != (node | KSM)? → 节点失效
│ │ │ └─ smp_rmb() 后检查 kpfn 是否变化(迁移中)
│ │ │ 变化 → goto again 重试
│ │ │ 未变化 → remove_node_from_stable_tree() 清理
│ │ └─ 成功获取 folio(引用计数 +1,持有锁)
│ │
│ └─ memcmp_pages(page, folio_page)
│ < 0 → 向左子树
│ > 0 → 向右子树
│ == 0 → 找到候选!
│
└─ 返回 kfolio(找到)或 NULL(未找到)
关键的 folio_set_stable_node() 函数(mm/ksm.c:1092-1097)将 folio 与 stable_node 关联:
static inline void folio_set_stable_node(struct folio *folio,
struct ksm_stable_node *stable_node)
{
folio->mapping = (void *)((unsigned long)stable_node | FOLIO_MAPPING_KSM);
}FOLIO_MAPPING_KSM 利用 folio->mapping 低位(指针对齐保证低 3 位为 0),将 KSM 标记嵌入指针中。folio_test_ksm() 就是检查此低位标志。
稳定树插入(stable_tree_insert(),mm/ksm.c:2039):
/* mm/ksm.c:2095-2100 节选 */
stable_node_dup->kpfn = kpfn;
stable_node_dup->rmap_hlist_len = 0;
DO_NUMA(stable_node_dup->nid = nid);
if (!need_chain) {
rb_link_node(&stable_node_dup->node, parent, new);
rb_insert_color(&stable_node_dup->node, root);
}特性:
- 存储"连续两次扫描内容未变化"的候选页(通过 checksum 判断稳定性)
- 节点为
ksm_rmap_item(嵌入rb_node)——节点即数据,无额外分配 - 每轮完整扫描开始时全部清空(
mm/ksm.c:2624-2625)
不稳定树搜索与插入(unstable_tree_search_insert(),mm/ksm.c:2133):
static struct ksm_rmap_item *unstable_tree_search_insert(
struct ksm_rmap_item *rmap_item, struct page *page,
struct page **tree_pagep)
{
struct rb_node **new;
struct rb_root *root;
...
nid = get_kpfn_nid(page_to_pfn(page));
root = root_unstable_tree + nid;
new = &root->rb_node;
while (*new) {
tree_rmap_item = rb_entry(*new, struct ksm_rmap_item, node);
tree_page = get_mergeable_page(tree_rmap_item);
...
ret = memcmp_pages(page, tree_page);
if (ret < 0) {
new = &parent->rb_left;
} else if (ret > 0) {
new = &parent->rb_right;
} else if (!ksm_merge_across_nodes &&
page_to_nid(tree_page) != nid) {
/* NUMA 节点不匹配:不合并 */
return NULL;
} else {
*tree_pagep = tree_page;
return tree_rmap_item; /* 找到相同页!*/
}
}
/* 未找到:将当前 rmap_item 插入树 */
rmap_item->address |= UNSTABLE_FLAG;
rmap_item->address |= (ksm_scan.seqnr & SEQNR_MASK);
DO_NUMA(rmap_item->nid = nid);
rb_link_node(&rmap_item->node, parent, new);
rb_insert_color(&rmap_item->node, root);
ksm_pages_unshared++;
return NULL;
}搜索和插入合并为一次树遍历,这正是红黑树的优势所在。
特性 稳定树 不稳定树
─────────────────────────────────────────────────────────────────────
节点类型 ksm_stable_node ksm_rmap_item(双用)
节点持有页引用 不持有(keyhole 机制) 不持有
生命周期 持久(直到页被释放) 每轮全部清空重建
页状态 写保护只读(KSM 页) 普通匿名页(可变)
比较键 页内容(memcmp_pages) 同左
树可靠性 完全可靠 不可靠(内容随时可变)
查找优先级 先查(更快,稳定) 稳定树未命中后才查
NUMA 支持 merge_across_nodes=0时分树 同左
最大节点数 无限(受物理内存限制) 受限(每轮清空)
chain/dup 支持(超过 max_page_sharing) 不涉及
搜索方式 只搜索(与插入分离) 搜索+插入合一(高效)
KSM 在系统初始化阶段通过 ksm_init()(mm/ksm.c:3975)创建 ksmd 线程:
/* mm/ksm.c:3975-4018 */
static int __init ksm_init(void)
{
struct task_struct *ksm_thread;
int err;
/* 计算空白页的 xxhash 校验值,供 use_zero_pages 优化 */
zero_checksum = calc_checksum(ZERO_PAGE(0));
ksm_use_zero_pages = false; /* 默认不启用零页优化 */
err = ksm_slab_init();
if (err)
goto out;
ksm_thread = kthread_run(ksm_scan_thread, NULL, "ksmd");
if (IS_ERR(ksm_thread)) { ... }
#ifdef CONFIG_SYSFS
err = sysfs_create_group(mm_kobj, &ksm_attr_group);
...
#endif
#ifdef CONFIG_MEMORY_HOTREMOVE
hotplug_memory_notifier(ksm_memory_callback, KSM_CALLBACK_PRI);
#endif
return 0;
}
subsys_initcall(ksm_init);subsys_initcall 确保 ksmd 在子系统初始化阶段启动,早于用户空间进程但晚于基本内存管理。
static int ksm_scan_thread(void *nothing)
{
unsigned int sleep_ms;
set_freezable(); /* 支持系统挂起/休眠时冻结 */
set_user_nice(current, 5); /* 低优先级,让出给用户进程 */
while (!kthread_should_stop()) {
mutex_lock(&ksm_thread_mutex);
wait_while_offlining(); /* 等待内存热插拔完成 */
if (ksmd_should_run())
ksm_do_scan(ksm_thread_pages_to_scan);
mutex_unlock(&ksm_thread_mutex);
if (ksmd_should_run()) {
sleep_ms = READ_ONCE(ksm_thread_sleep_millisecs);
wait_event_freezable_timeout(ksm_iter_wait,
sleep_ms != READ_ONCE(ksm_thread_sleep_millisecs),
msecs_to_jiffies(sleep_ms)); /* 休眠 20ms */
} else {
wait_event_freezable(ksm_thread_wait,
ksmd_should_run() || kthread_should_stop()); /* 等待激活 */
}
}
return 0;
}ksmd_should_run() 的判断条件(mm/ksm.c:2796-2799):
static int ksmd_should_run(void)
{
return (ksm_run & KSM_RUN_MERGE) && !list_empty(&ksm_mm_head.slot.mm_node);
}必须同时满足:(1)ksm_run 设置了 KSM_RUN_MERGE 标志;(2)mm_slot 链表非空(有进程参与 KSM)。
static void ksm_do_scan(unsigned int scan_npages)
{
struct ksm_rmap_item *rmap_item;
struct page *page;
while (scan_npages-- && likely(!freezing(current))) {
cond_resched(); /* 主动让出 CPU,防止延迟 */
rmap_item = scan_get_next_rmap_item(&page); /* 获取下一个候选页 */
if (!rmap_item)
return; /* 本轮扫描完毕 */
cmp_and_merge_page(page, rmap_item); /* 比较并尝试合并 */
put_page(page);
ksm_pages_scanned++;
}
}scan_npages 即 ksm_thread_pages_to_scan,默认 100(DEFAULT_PAGES_TO_SCAN,mm/ksm.c:252)。每扫描一页都调用 cond_resched() 检查是否需要调度,保证 ksmd 不占用 CPU 过长。
此函数是 ksmd 扫描引擎的核心,实现断点续扫(从上次停止的位置继续):
scan_get_next_rmap_item() 执行流程:
1. 若 mm_slot == &ksm_mm_head(新轮次开始):
a. 清空所有不稳定树:root_unstable_tree[nid] = RB_ROOT
b. lru_add_drain_all():排干 LRU 缓存(确保页计数正确)
c. advisor_start_scan():记录扫描开始时间(用于 EWMA advisor)
d. 移到第一个真实 mm_slot
2. 扫描当前 mm_slot 的 VMA:
a. 检查 mm 是否已退出(ksm_test_exit())
b. mmap_read_lock(mm)
c. 遍历 VMA(通过 walk_page_range_vma)
d. 找到下一个匿名页(跳过非匿名、非 MERGEABLE VMA)
e. get_next_rmap_item():创建或复用 rmap_item
f. 若 smart_scan && should_skip_rmap_item():跳过此页
g. 返回 rmap_item 和对应 page
3. 若当前 mm 扫描完毕:
a. 释放 mm 无用的 rmap_item(老 rmap_item 不再被引用)
b. 若 mm 已退出:清理 mm_slot,mmdrop(mm)
c. 移动到链表中的下一个 mm_slot
4. 若已回到 ksm_mm_head(完成一轮完整扫描):
a. ksm_scan.seqnr++
b. advisor_stop_scan():更新 EWMA 调速
c. 返回 NULL(告知 ksm_do_scan 本轮结束)
扫描位置的持久化:
ksm_scan.address 和 ksm_scan.mm_slot 在每次 ksm_do_scan() 返回后保留,下次调用时从断点继续。这使得每批 100 个页面的扫描是精确连续的,不会漏掉或重复扫描。
KSM 使用 xxhash32 作为页面校验和(mm/ksm.c:28:#include <linux/xxhash.h>):
static u32 calc_checksum(struct page *page)
{
u32 checksum;
void *addr = kmap_local_page(page);
checksum = xxhash32(addr, PAGE_SIZE, 17);
kunmap_local(addr);
return checksum;
}两次扫描策略(mm/ksm.c:2280-2290):
第一次扫描候选页:
计算 checksum → 与 rmap_item->oldchecksum 比较
不同 → 更新 oldchecksum,返回(页内容不稳定,等下次)
相同 → 页内容两次扫描都一样,加入不稳定树候选
只有 checksum 连续相同的页才会进入不稳定树,这大幅减少了不必要的 memcmp_pages() 调用(memcmp 需要全量比对 4096 字节,而 xxhash 是轻量级散列)。
xxhash vs memcmp 开销对比:
xxhash32(4KB 页):约 50-100 ns(可能 CPU 缓存命中)
memcmp_pages(4KB):约 200-500 ns(需完整扫描页内容)
→ checksum 过滤使 memcmp 调用减少 ~90%
cmp_and_merge_page(page, rmap_item)
│
├─ [页已是 KSM 页?folio_stable_node(folio) != NULL]
│ 是 → 检查 NUMA 节点是否匹配 stable_node->nid
│ 不匹配 → 将 stable_node 移入 migrate_nodes 列表
│ 若 rmap_item->head == stable_node(已稳定)→ 直接返回
│
└─ [普通匿名页]
│
├─ remove_rmap_item_from_tree(rmap_item) 从上轮不稳定树清除
│
├─ checksum = calc_checksum(page)
├─ [checksum != rmap_item->oldchecksum?]
│ 是 → 更新 checksum,返回(页内容不稳定)
│
├─ [use_zero_pages && checksum == zero_checksum?]
│ 是 → try_to_merge_with_zero_page() → 成功则返回
│
├─ kfolio = stable_tree_search(page)
│ 命中 → try_to_merge_with_ksm_page(rmap_item, page, kpage)
│ 成功 → stable_tree_append(rmap_item, stable_node)
│ 返回
│
└─ [稳定树未命中]
unstable_tree_search_insert(rmap_item, page, &tree_page)
命中 tree_rmap_item →
kfolio = try_to_merge_two_pages(...)
成功 → stable_node = stable_tree_insert(kfolio)
stable_tree_append(tree_rmap_item, stable_node)
stable_tree_append(rmap_item, stable_node)
失败 → break_cow(tree_rmap_item); break_cow(rmap_item)
未命中 → rmap_item 已插入不稳定树,等待下次匹配
合并的第一步是将候选页设置为写保护。完整流程:
static int write_protect_page(struct vm_area_struct *vma, struct folio *folio,
pte_t *orig_pte)
{
/* 1. 通过 page_vma_mapped_walk() 找到 PTE */
DEFINE_FOLIO_VMA_WALK(pvmw, folio, vma, 0, 0);
pvmw.address = page_address_in_vma(folio, ...);
/* 2. 通知 MMU notifier(通知 KVM 等虚拟化层)*/
mmu_notifier_invalidate_range_start(&range);
page_vma_mapped_walk(&pvmw);
entry = ptep_get(pvmw.pte);
/* 3. 若 PTE 可写、脏,或有独占位,需要写保护 */
if (pte_write(entry) || pte_dirty(entry) ||
anon_exclusive || mm_tlb_flush_pending(mm)) {
/* 4. 清除 PTE 并刷新 TLB(在检查前,防止 O_DIRECT 竞争)*/
entry = ptep_clear_flush(vma, pvmw.address, pvmw.pte);
/* 5. 关键检查:确保没有正在进行的 O_DIRECT I/O */
if (folio_mapcount(folio) + 1 + swapped != folio_ref_count(folio)) {
set_pte_at(mm, pvmw.address, pvmw.pte, entry);
goto out_unlock; /* 有其他引用,放弃 */
}
/* 6. 设置写保护 */
entry = pte_mkclean(entry);
entry = pte_wrprotect(entry);
set_pte_at(mm, pvmw.address, pvmw.pte, entry);
}
*orig_pte = entry;
err = 0;
}步骤 5 的 O_DIRECT 竞争检测(mm/ksm.c:1331)是一个精妙的安全检查:
folio_ref_count =
folio_mapcount (来自 PTE 的映射引用)
+ 1 (我们自己的 get_page 引用)
+ swapped (swapcache 引用,若在 swap)
若等式成立 → 无其他引用(如 DMA/O_DIRECT)→ 安全写保护
若不等 → 有 O_DIRECT I/O 在使用此页 → 放弃
注释(mm/ksm.c:1313-1319)专门解释了为何要先清除 PTE 再检查:清除 PTE 后,任何新的 O_DIRECT 请求都无法再获得该页,从而消除了检查-写入之间的竞争窗口。
将候选页的 PTE 替换为指向 KSM 共享页:
static int replace_page(struct vm_area_struct *vma, struct page *page,
struct page *kpage, pte_t orig_pte)
{
addr = page_address_in_vma(folio, page, vma);
pmd = mm_find_pmd(mm, addr);
mmu_notifier_invalidate_range_start(&range);
ptep = pte_offset_map_lock(mm, pmd, addr, &ptl);
/* 验证 PTE 未被并发修改 */
if (!pte_same(ptep_get(ptep), orig_pte))
goto out_mn;
if (!is_zero_pfn(page_to_pfn(kpage))) {
/* 普通 KSM 页 */
folio_get(kfolio); /* +1 引用 */
folio_add_anon_rmap_pte(kfolio, kpage, vma, addr, RMAP_NONE);
newpte = mk_pte(kpage, vma->vm_page_prot);
newpte = pte_wrprotect(newpte); /* 写保护 */
newpte = pte_mkclean(newpte);
} else {
/* 零页特殊处理 */
newpte = pte_mkdirty(pte_mkspecial(pfn_pte(...)));
ksm_map_zero_page(mm);
dec_mm_counter(mm, MM_ANONPAGES);
}
ptep_clear_flush(vma, addr, ptep); /* 清除旧 PTE,刷新 TLB */
set_pte_at(mm, addr, ptep, newpte); /* 写入新 PTE(指向 KSM 页)*/
folio_remove_rmap_pte(folio, page, vma); /* 移除旧页的 rmap 记录 */
folio_put(folio); /* 释放旧页引用(可能触发释放)*/
}PTE 替换后,旧页的引用计数降为 0,内核自动释放物理页,实现内存节省。
static int try_to_merge_one_page(struct vm_area_struct *vma,
struct page *page, struct page *kpage)
{
if (page == kpage) return 0; /* fork 的 ksm 页,无需合并 */
if (!folio_test_anon(folio)) goto out; /* 必须是匿名页 */
if (!folio_trylock(folio)) goto out; /* 非阻塞锁,失败则跳过 */
/* 大页需要先拆分 */
if (folio_test_large(folio)) {
if (split_huge_page(page))
goto out_unlock; /* 拆分失败,放弃 */
folio = page_folio(page);
}
if (write_protect_page(vma, folio, &orig_pte) == 0) {
if (!kpage) {
/* kpage=NULL:仅写保护,升级为 KSM 候选(try_to_merge_two_pages 第一步)*/
folio_set_stable_node(folio, NULL); /* 设置 KSM 标志,但 stable_node=NULL */
folio_mark_accessed(folio);
if (!folio_test_dirty(folio))
folio_mark_dirty(folio); /* 确保 KSM 页会被 swap */
err = 0;
} else if (pages_identical(page, kpage))
err = replace_page(vma, page, kpage, orig_pte);
}
folio_unlock(folio);
return err;
}kpage=NULL 的情况是 try_to_merge_two_pages() 的第一步:将候选页"升级"为写保护的 KSM 候选,此时 folio_set_stable_node(folio, NULL) 设置了 KSM 标志但尚未关联任何 stable_node,待 stable_tree_insert() 成功后再关联。
static struct folio *try_to_merge_two_pages(
struct ksm_rmap_item *rmap_item, struct page *page,
struct ksm_rmap_item *tree_rmap_item, struct page *tree_page)
{
int err;
/* 步骤 1:将 page 写保护并升级为 KSM 候选(kpage=NULL)*/
err = try_to_merge_with_ksm_page(rmap_item, page, NULL);
if (!err) {
/* 步骤 2:将 tree_page 合并到 page(page 已是写保护 KSM 候选)*/
err = try_to_merge_with_ksm_page(tree_rmap_item, tree_page, page);
if (err)
break_cow(rmap_item); /* 步骤 2 失败:撤销步骤 1 */
}
return err ? NULL : page_folio(page);
}两步合并策略:第一步写保护 page,第二步将 tree_page 的 PTE 替换为指向 page。若第二步失败(如并发修改),通过 break_cow(rmap_item) 撤销第一步,恢复 page 为普通匿名页。
cmp_and_merge_page()
│
├─ [与已有 KSM 页合并]
│ try_to_merge_with_ksm_page(rmap_item, page, kpage) [mm/ksm.c:1579]
│ mmap_read_lock(mm)
│ find_mergeable_vma()
│ try_to_merge_one_page(vma, page, kpage) [mm/ksm.c:1475]
│ folio_trylock()
│ split_huge_page()(大页时)
│ write_protect_page() [mm/ksm.c:1272]
│ ptep_clear_flush() → TLB 刷新
│ pte_wrprotect() → 写保护
│ set_pte_at()
│ replace_page() [mm/ksm.c:1372]
│ ptep_clear_flush() → 再次刷新 TLB
│ set_pte_at(newpte) → 写入新 PTE
│ folio_remove_rmap_pte()
│ folio_put()(释放旧页)
│ get_anon_vma(vma->anon_vma) → 保持 anon_vma 引用
│ stable_tree_append() [mm/ksm.c:2205]
│
└─ [两个新页首次合并]
unstable_tree_search_insert() [mm/ksm.c:2133]
try_to_merge_two_pages() [mm/ksm.c:1618]
try_to_merge_with_ksm_page(rmap_item, page, NULL)
→ write_protect_page() + folio_set_stable_node(NULL)
try_to_merge_with_ksm_page(tree_rmap_item, tree_page, page)
→ write_protect_page() + replace_page()
stable_tree_insert(kfolio) [mm/ksm.c:2039]
stable_tree_append(tree_rmap_item, ...)
stable_tree_append(rmap_item, ...)
KSM 页被设置为写保护后,任何进程尝试写入该地址都会触发缺页异常。内核缺页路径:
进程写入 KSM 页(写保护触发缺页)
│
▼
handle_mm_fault() (mm/memory.c)
└─ do_wp_page() (mm/memory.c)
│
├─ folio_test_ksm(folio)? 是 KSM 页
│ │
│ └─ FAULT_FLAG_UNSHARE 模式
│ wp_page_copy()
│ 分配新页(vma_alloc_folio)
│ copy_mc_user_highpage() 复制 KSM 页内容
│ 设置新 PTE(可写)
│ folio_remove_rmap_pte(kfolio) 从 KSM 页解除 rmap
│ stable_node->rmap_hlist_len-- (下次 ksmd 扫描更新)
│
└─ 进程获得私有可写的独立页
COW 后的内存状态变化:
COW 前:
进程A PTE → KSM 页(写保护)←← 进程B PTE
│
stable_node
│ hlist
rmap_item_A → rmap_item_B
COW 后(进程A 写入):
进程A PTE → 新私有页(可写) 进程B PTE → KSM 页(写保护)
│
stable_node
│ hlist
rmap_item_B
(rmap_item_A 将在下次 ksmd 扫描时被发现并清理)
当 KSM 页经历多次写入(多个进程都触发 COW 解离),同一内容的数据可能被合并到不同物理页上(每次合并时系统可能选择不同的物理帧作为共享页)。此时通过 chain/dup 结构处理:
/* mm/ksm.c:1659-1700 stable_node_dup() 函数 */
static struct folio *stable_node_dup(struct ksm_stable_node **_stable_node_dup,
struct ksm_stable_node **_stable_node,
struct rb_root *root,
bool prune_stale_stable_nodes)
{
/* 遍历 chain 下的所有 dup 节点,找到最优候选 */
hlist_for_each_entry_safe(dup, hlist_safe, &stable_node->hlist, hlist_dup) {
...
/* 检查 dup 节点对应的物理页是否仍然有效 */
folio = ksm_get_folio(dup, KSM_GET_FOLIO_NOLOCK);
if (!folio) {
/* 失效节点:清理 */
free_stable_node(dup);
continue;
}
/* 选择 rmap_hlist_len 最大(有最多映射)的有效 dup */
if (folio && dup->rmap_hlist_len > found_rmap_hlist_len) {
...
found = dup;
found_rmap_hlist_len = dup->rmap_hlist_len;
}
}
*_stable_node_dup = found;
return tree_folio;
}当曾经是 KSM 页的数据被换出(swap out)再换入时,需要特殊处理:
struct folio *ksm_might_need_to_copy(struct folio *folio,
struct vm_area_struct *vma, unsigned long addr)
{
/* 大页直接返回,不处理 */
if (folio_test_large(folio))
return folio;
if (folio_test_ksm(folio)) {
if (folio_stable_node(folio) && !(ksm_run & KSM_RUN_UNMERGE))
return folio; /* 仍然是有效的 KSM 页,直接使用 */
} else if (!anon_vma) {
return folio; /* 无 anon_vma,无需复制 */
} else if (folio->index == linear_page_index(vma, addr) &&
anon_vma->root == vma->anon_vma->root) {
return folio; /* 同 anon_vma 树,无需复制 */
}
/* 需要复制:page 不能安全地回到这个 VMA */
if (PageHWPoison(page)) return ERR_PTR(-EHWPOISON);
if (!folio_test_uptodate(folio)) return folio; /* 让 do_swap_page 报错 */
new_folio = vma_alloc_folio(GFP_HIGHUSER_MOVABLE, 0, vma, addr);
if (new_folio && mem_cgroup_charge(new_folio, vma->vm_mm, GFP_KERNEL)) {
folio_put(new_folio);
new_folio = NULL;
}
if (new_folio) {
copy_mc_user_highpage(folio_page(new_folio, 0), page, addr, vma);
folio_set_dirty(new_folio);
...
#ifdef CONFIG_SWAP
count_vm_event(KSM_SWPIN_COPY);
#endif
}
return new_folio;
}为何 swap-in 时需要复制(来自 include/linux/ksm.h:83-92 的注释):
* When do_swap_page() first faults in from swap what used to be a KSM page,
* no problem, it will be assigned to this vma's anon_vma; but thereafter,
* it might be faulted into a different anon_vma (or perhaps to a different
* offset in the same anon_vma). do_swap_page() cannot do all the locking
* needed to reconstitute a cross-anon_vma KSM page: for now it has to make
* a copy, and leave remerging the pages to a later pass of ksmd.
换入时 do_swap_page() 无法重建跨 anon_vma 的 KSM 映射(需要太多锁),因此强制复制一份,留给 ksmd 下次扫描时重新合并。
static int break_ksm(struct vm_area_struct *vma, unsigned long addr,
unsigned long end, bool lock_vma)
{
vm_fault_t ret = 0;
const struct mm_walk_ops *ops = lock_vma ?
&break_ksm_lock_vma_ops : &break_ksm_ops;
do {
cond_resched();
ksm_page = walk_page_range_vma(vma, addr, end, ops, &addr);
if (ksm_page <= 0)
return ksm_page;
/* 通过 FAULT_FLAG_UNSHARE 触发 COW,使页私有化 */
ret = handle_mm_fault(vma, addr,
FAULT_FLAG_UNSHARE | FAULT_FLAG_REMOTE, NULL);
} while (!(ret & (VM_FAULT_SIGBUS | VM_FAULT_SIGSEGV | VM_FAULT_OOM)));
return (ret & VM_FAULT_OOM) ? -ENOMEM : 0;
}循环调用直到不再找到 KSM 页(handle_mm_fault() 可能因并发而退出,需重试)。MADV_UNMERGEABLE 和 KSM_RUN_UNMERGE 均通过此路径解除合并。
/* mm/ksm.c:318-331 */
/**
* struct advisor_ctx - metadata for KSM advisor
* @start_scan: start time of the current scan
* @scan_time: scan time of previous scan
* @change: change in percent to pages_to_scan parameter
* @cpu_time: cpu time consumed by the ksmd thread in the previous scan
*/
struct advisor_ctx {
ktime_t start_scan;
unsigned long scan_time;
unsigned long change;
unsigned long long cpu_time;
};
static struct advisor_ctx advisor_ctx;两种 advisor 模式(mm/ksm.c:333-338):
enum ksm_advisor_type {
KSM_ADVISOR_NONE, /* 手动控制:pages_to_scan 固定 */
KSM_ADVISOR_SCAN_TIME, /* 自动控制:按目标扫描时间调整 */
};/* Exponentially weighted moving average */
#define EWMA_WEIGHT 30
static unsigned long ewma(unsigned long prev, unsigned long curr)
{
return ((100 - EWMA_WEIGHT) * prev + EWMA_WEIGHT * curr) / 100;
}EWMA 权重 30% 意味着:新值占 30%,历史值占 70%。这使得 pages_to_scan 的变化较为平滑,避免因单次扫描时间波动导致的剧烈抖动。
static void scan_time_advisor(void)
{
/* 1. 计算本次扫描耗时(秒)*/
scan_time = div_s64(ktime_ms_delta(ktime_get(), advisor_ctx.start_scan),
MSEC_PER_SEC);
scan_time = scan_time ? scan_time : 1;
/* 2. 计算 ksmd 本次消耗的 CPU 时间(毫秒)*/
cpu_time = task_sched_runtime(current);
cpu_time_diff = cpu_time - advisor_ctx.cpu_time;
cpu_time_diff_ms = cpu_time_diff / 1000 / 1000;
/* 3. 计算 CPU 使用率百分比 */
cpu_percent = (cpu_time_diff_ms * 100) / (scan_time * 1000);
cpu_percent = cpu_percent ? cpu_percent : 1;
/* 4. 计算目标扫描时间与实际扫描时间的比值 */
factor = ksm_advisor_target_scan_time * 100 / scan_time;
factor = factor ? factor : 1;
/* 5. 计算变化因子(EWMA 平滑)*/
change = scan_time * 100 / last_scan_time;
change = ewma(advisor_ctx.change, change);
/* 6. 新的 pages_to_scan */
pages = ksm_thread_pages_to_scan * 100 / factor;
pages = pages * change / 100;
/* 7. CPU 限制(不超过 ksm_advisor_max_cpu = 70%)*/
per_page_cost = ksm_thread_pages_to_scan / cpu_percent;
pages = min(pages, per_page_cost * ksm_advisor_max_cpu);
pages = max(pages, per_page_cost * KSM_ADVISOR_MIN_CPU); /* 最小 10% CPU */
/* 8. 绝对值上下界 */
pages = min(pages, ksm_advisor_max_pages_to_scan); /* 默认 30000 */
pages = max(pages, ksm_advisor_min_pages_to_scan); /* 默认 500 */
ksm_thread_pages_to_scan = pages;
trace_ksm_advisor(scan_time, pages, cpu_percent);
}调速算法可视化:
目标扫描时间 = 200 秒
实际扫描时间 = 400 秒(太慢)
→ factor = 200/400 * 100 = 50
→ pages = current_pages * 100/50 = current_pages * 2
→ pages_to_scan 翻倍(加快扫描)
实际扫描时间 = 100 秒(太快,占用 CPU 过多)
→ factor = 200/100 * 100 = 200
→ pages = current_pages * 100/200 = current_pages * 0.5
→ pages_to_scan 减半(减少 CPU 占用)
CPU 使用率约束(70%)会进一步限制上界,防止 ksmd 独占 CPU
/sys/kernel/mm/ksm/advisor_mode none(手动)或 scan-time(自动)
/sys/kernel/mm/ksm/advisor_target_scan_time 目标扫描时间(秒,默认 200)
/sys/kernel/mm/ksm/advisor_max_cpu 最大 CPU 使用率(默认 70%)
/sys/kernel/mm/ksm/advisor_min_pages_to_scan 最小批次大小(默认 500)
/sys/kernel/mm/ksm/advisor_max_pages_to_scan 最大批次大小(默认 30000)
设置建议:
# 虚拟化主机:快速扫描,允许较高 CPU
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
echo 60 > /sys/kernel/mm/ksm/advisor_target_scan_time
echo 80 > /sys/kernel/mm/ksm/advisor_max_cpu
# 生产服务器:保守设置,减少对业务影响
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
echo 300 > /sys/kernel/mm/ksm/advisor_target_scan_time
echo 30 > /sys/kernel/mm/ksm/advisor_max_cpuskip_age() 函数(mm/ksm.c:2424)根据 rmap_item 的扫描年龄决定跳过次数:
static unsigned int skip_age(rmap_age_t age)
{
if (age <= 3) return 1; /* 新页:每次都扫描 */
if (age <= 5) return 2; /* 较新:每 2 次扫一次 */
if (age <= 8) return 4; /* 中等:每 4 次扫一次 */
return 8; /* 老页:每 8 次扫一次 */
}should_skip_rmap_item()(mm/ksm.c:2442)的完整逻辑:
static bool should_skip_rmap_item(struct folio *folio,
struct ksm_rmap_item *rmap_item)
{
if (!ksm_smart_scan) return false; /* 功能未启用 */
if (folio_test_ksm(folio)) return false; /* KSM 页永不跳过 */
age = rmap_item->age;
if (age != U8_MAX) rmap_item->age++; /* 增加年龄(最大 255)*/
if (age < 3) return false; /* 年轻页不跳过 */
if (!rmap_item->remaining_skips) {
rmap_item->remaining_skips = skip_age(age);
return false; /* 重置跳过计数,本次不跳过 */
}
ksm_pages_skipped++;
rmap_item->remaining_skips--;
remove_rmap_item_from_tree(rmap_item);
return true; /* 跳过此页 */
}智能扫描使多次尝试去重失败的页面按指数退避减少扫描频率,最多每 8 次扫描才检查一次。这在大量"无法合并"页面存在时显著减少 CPU 开销。
/* mm/ksm.c:469-476 */
#ifdef CONFIG_NUMA
/* Zeroed when merging across nodes is not allowed */
static unsigned int ksm_merge_across_nodes = 1;
static int ksm_nr_node_ids = 1;
#else
#define ksm_merge_across_nodes 1U
#define ksm_nr_node_ids 1
#endifksm_merge_across_nodes=1(默认):所有 NUMA 节点共享一棵稳定树和一棵不稳定树。
ksm_merge_across_nodes=0:每个 NUMA 节点有独立的稳定树和不稳定树数组。
NUMA 0(本地) NUMA 1(远端)
进程A VMA 进程B VMA
│ │
└─────── KSM 共享页(在 NUMA 0 上)──────────────┘
↑
进程B 每次访问需要跨 NUMA 内存总线
延迟:本地 ~50ns,远端 ~150-200ns(3-4倍)
当 merge_across_nodes=0 时,两个 NUMA 节点上内容相同的页不会被合并(即使内容完全一致)。不稳定树搜索时,若发现 NUMA 节点不匹配则拒绝合并(mm/ksm.c:2175-2183):
} else if (!ksm_merge_across_nodes &&
page_to_nid(tree_page) != nid) {
/*
* If tree_page has been migrated to another NUMA node,
* it will be flushed out and put in the right unstable
* tree next time: only merge with it when across_nodes.
*/
put_page(tree_page);
return NULL;
}NUMA 场景权衡:
merge_across_nodes=1(默认):
优点:合并率更高,内存节省更多
缺点:部分进程可能跨 NUMA 访问,增加内存延迟
merge_across_nodes=0:
优点:保证所有内存访问都是 NUMA 本地的
缺点:合并率降低(约 25-50% 的合并机会被放弃)
适合:对延迟敏感的 HPC、实时系统
KSM 页可能因 NUMA 内存自动均衡(NUMA Balancing)而迁移到不同节点。迁移后 stable_node 的 nid 与页实际所在 NUMA 节点不匹配,内核通过 migrate_nodes 列表暂存:
/* mm/ksm.c:233-235 */
static LIST_HEAD(migrate_nodes);
#define STABLE_NODE_DUP_HEAD ((struct list_head *)&migrate_nodes.prev)在 cmp_and_merge_page() 中(mm/ksm.c:2261-2267),若发现节点不匹配:
if (stable_node->head != &migrate_nodes &&
get_kpfn_nid(READ_ONCE(stable_node->kpfn)) != NUMA(stable_node->nid)) {
stable_node_dup_del(stable_node);
stable_node->head = &migrate_nodes;
list_add(&stable_node->list, stable_node->head);
}每轮扫描开始时遍历 migrate_nodes,重新验证并重新放置到正确 NUMA 节点的稳定树中,或清理失效节点。
CONFIG_NUMA + merge_across_nodes=0 时:
root_stable_tree[0](NUMA node 0)
└── ksm_stable_node {nid=0, kpfn=0x1000}
└── ksm_stable_node {nid=0, kpfn=0x2000}
...
root_stable_tree[1](NUMA node 1)
└── ksm_stable_node {nid=1, kpfn=0x80001000}
└── ksm_stable_node {nid=1, kpfn=0x80002000}
...
root_unstable_tree[0](每轮清空)
└── ksm_rmap_item {nid=0, address=0xdeadbeef000}
...
root_unstable_tree[1](每轮清空)
...
KSM 的页面共享机制带来了侧信道攻击面。经典的 Flush+Reload 攻击流程:
攻击场景(多租户 KVM 主机):
Guest A(攻击者) Guest B(受害者)
│ │
│ 1. 确认 KSM 已合并某页 │
│ (通过合并前后 COW 时 │
│ 间差判断) │
│ │
│ 2. clflush 该页(驱逐 │
│ CPU 缓存) │
│ │ │ 3. 受害者访问该 KSM 页
│ │ → 页被载入 LLC 缓存
│ │
│ 4. 测量 reload 时间 │
│ < 50ns → LLC 命中 │
│ = B 最近访问了该页 │
│ > 200ns → 缓存未命中 │
│ = B 没有访问该页 │
攻击者可以以约 1-10 MHz 的频率探测受害者的内存访问模式,在某些情况下可重建 AES 密钥(通过侧信道推断 T-table 访问模式)。
参考研究:
- Yarom & Falkner, "FLUSH+RELOAD: a High Resolution, Low Noise, L3 Cache Side-Channel Attack", USENIX Security 2014
- Irazoqui et al., "S$A: A Shared Cache Attack That Works across Cores and Defies VM Sandboxing", IEEE S&P 2015
Rowhammer 攻击通过高频访问相邻内存行诱发位翻转。KSM 与 Rowhammer 有间接关联:
传统 Rowhammer:
攻击者占用物理内存 Row A 和 Row C
高频访问 → 诱发 Row B(目标)位翻转
KSM + Rowhammer:
KSM 合并后,攻击者和受害者共享同一物理页
攻击者可以 COW 出一份写保护页的副本
通过 Rowhammer 影响共享的物理行(受害者数据所在行)
→ 在某些内存拓扑下,扩大了攻击面
Linux 5.18 引入了进程级 KSM 控制,不需要逐个 VMA 调用 madvise():
/* include/linux/ksm.h:22-23 */
int ksm_enable_merge_any(struct mm_struct *mm);
int ksm_disable_merge_any(struct mm_struct *mm);ksm_enable_merge_any()(mm/ksm.c:2916)的实现:
int ksm_enable_merge_any(struct mm_struct *mm)
{
int err;
if (mm_flags_test(MMF_VM_MERGE_ANY, mm))
return 0; /* 已启用,幂等操作 */
if (!mm_flags_test(MMF_VM_MERGEABLE, mm)) {
err = __ksm_enter(mm); /* 注册到 KSM 扫描链表 */
if (err)
return err;
}
mm_flags_set(MMF_VM_MERGE_ANY, mm);
ksm_add_vmas(mm); /* 对所有兼容 VMA 设置 VM_MERGEABLE 标志 */
return 0;
}通过 prctl(2) 调用:
/* kernel/sys.c */
case PR_SET_MEMORY_MERGE:
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
if (arg2)
return ksm_enable_merge_any(current->mm);
else
return ksm_disable_merge_any(current->mm);注意需要 CAP_SYS_ADMIN 权限(或特定内核配置),防止普通进程滥用 KSM 影响系统性能。
MMF_VM_MERGE_ANY 与 MMF_VM_MERGEABLE 的区别:
MMF_VM_MERGEABLE:进程已注册到 KSM 扫描(至少一个 VMA 参与)
MMF_VM_MERGE_ANY:进程希望所有兼容 VMA 都自动标记为 VM_MERGEABLE
→ madvise(MADV_MERGEABLE) 只设置 MMF_VM_MERGEABLE(针对特定 VMA)
→ prctl(PR_SET_MEMORY_MERGE) 设置 MMF_VM_MERGE_ANY(全进程自动化)
新建 VMA 时:ksm_vma_flags() 检查 MMF_VM_MERGE_ANY,
自动在 vm_flags 中加入 VM_MERGEABLE(mm/ksm.c:2867-2878)
内核层面的现有缓解:
ksm_max_page_sharing(默认 256):限制单个 KSM 页的最大共享数,减少攻击面use_zero_pages=0(默认):零页通过普通 KSM 流程合并而非直接映射已知的零页,减少可预测合并目标 3smart_scan:减少扫描频率,间接降低信息泄露速率
配置层面的建议:
# 高安全要求场景:完全禁用 KSM
echo 0 > /sys/kernel/mm/ksm/run
# 折中方案:禁用跨租户 NUMA 节点合并
echo 0 > /sys/kernel/mm/ksm/merge_across_nodes
# 容器场景:仅允许已知安全工作负载使用 KSM
# 通过 cgroup v2 或进程启动时 prctl 精确控制KSM_MERGE_DEFAULT vs KSM_MERGE_DISCARD(概念层面):
在某些内核配置和讨论中,这两个概念对应:
KSM_MERGE_DEFAULT(即MADV_MERGEABLE):进程主动声明区域可合并KSM_MERGE_DISCARD(即KSM_RUN_UNMERGE):系统级解除所有合并(安全审计后的应急处置)
内核文档(Documentation/admin-guide/mm/ksm.rst)明确警告:在多租户环境中使用 KSM 需要评估侧信道风险,尤其是当不同安全级别的工作负载在同一主机上运行时。
所有统计指标通过 /sys/kernel/mm/ksm/ 暴露:
指标 内核变量 含义
───────────────────────────────────────────────────────────────────────
pages_shared ksm_pages_shared 稳定树中的 KSM 节点数(独立物理页数)
pages_sharing ksm_pages_sharing 额外共享此节点的映射数
pages_unshared ksm_pages_unshared 不稳定树中的节点数(等待匹配的候选)
pages_volatile computed 已跟踪但频繁变化的页数
pages_skipped ksm_pages_skipped smart_scan 跳过的页数(累计)
ksm_zero_pages ksm_zero_pages KSM 映射的零页总数(原子计数)
full_scans ksm_scan.seqnr 完整扫描轮次
general_profit computed 整体节省内存估算(字节)
stable_node_chains ksm_stable_node_chains chain 节点数
stable_node_dups ksm_stable_node_dups dup 节点数
pages_scanned ksm_pages_scanned 总扫描页数(累计)
static ssize_t pages_volatile_show(struct kobject *kobj, ...)
{
long ksm_pages_volatile;
ksm_pages_volatile = ksm_rmap_items - ksm_pages_shared
- ksm_pages_sharing - ksm_pages_unshared;
/*
* It was not worth any locking to calculate that statistic,
* but it might therefore sometimes be negative: conceal that.
*/
if (ksm_pages_volatile < 0)
ksm_pages_volatile = 0;
return sysfs_emit(buf, "%ld\n", ksm_pages_volatile);
}pages_volatile = 总 rmap_item 数 - 稳定 - 分享 - 不稳定 = "既未合并也未进入不稳定树"的页数(通常是内容频繁变化,每次扫描 checksum 都不同的页)。
general_profit(mm/ksm.c:3734-3743):
static ssize_t general_profit_show(struct kobject *kobj, ...)
{
long general_profit;
general_profit = (ksm_pages_sharing + atomic_long_read(&ksm_zero_pages)) * PAGE_SIZE -
ksm_rmap_items * sizeof(struct ksm_rmap_item);
return sysfs_emit(buf, "%ld\n", general_profit);
}公式分解:
ksm_pages_sharing * PAGE_SIZE:通过 KSM 合并节省的字节数(sharing 个页面不再占用独立物理内存)ksm_zero_pages * PAGE_SIZE:零页合并额外节省的字节数ksm_rmap_items * sizeof(ksm_rmap_item):KSM 元数据消耗的字节数(成本)
注意区分 pages_shared 与 pages_sharing:
举例:3 个进程共享同一个 KSM 页(内容相同的只读数据段)
物理内存状态:
KSM 页(1 个物理页) ← PTE_A(进程1)
← PTE_B(进程2)
← PTE_C(进程3)
→ pages_shared += 1(多了 1 个稳定树节点)
→ pages_sharing += 2(第 2、3 个映射才计入 sharing)
节省的物理内存 = pages_sharing * PAGE_SIZE = 2 * 4KB = 8KB
(第 1 个映射是"共享页"本身,第 2、3 个映射指向它,节省了 2 个物理页)
进程级节省(mm/ksm.c:3440-3444):
long ksm_process_profit(struct mm_struct *mm)
{
return (long)(mm->ksm_merging_pages + mm_ksm_zero_pages(mm)) * PAGE_SIZE -
mm->ksm_rmap_items * sizeof(struct ksm_rmap_item);
}通过 /proc/<pid>/ksm_stat 查看进程级 KSM 统计(需要 CONFIG_PROC_FS)。
total_rmap_items = pages_shared + pages_sharing + pages_unshared + pages_volatile
= ksm_rmap_items(总 rmap_item 数)
节省的物理内存 ≈ pages_sharing * 4KB + ksm_zero_pages * 4KB
KSM 元数据开销 = ksm_rmap_items * ~40 字节
+ ksm_stable_node_chains * ~48 字节
+ ksm_stable_node_dups * ~48 字节
净收益(general_profit)= 节省物理内存 - KSM 元数据开销
理想状态:general_profit >> 0(节省大于开销)
警告状态:general_profit < 0(开销超过节省,需调整)
madvise(MADV_MERGEABLE) 由用户空间应用程序主动调用,声明某个地址范围是适合 KSM 合并的候选区域:
系统调用路径:
sys_madvise(addr, length, MADV_MERGEABLE)
→ madvise_vma_behavior() (mm/madvise.c:1416)
→ ksm_madvise(vma, start, end, MADV_MERGEABLE, &vm_flags)
(mm/ksm.c:2975)
ksm_madvise() 的实现(mm/ksm.c:2975-3010):
int ksm_madvise(struct vm_area_struct *vma, unsigned long start,
unsigned long end, int advice, vm_flags_t *vm_flags)
{
struct mm_struct *mm = vma->vm_mm;
int err;
switch (advice) {
case MADV_MERGEABLE:
if (vma->vm_flags & VM_MERGEABLE)
return 0; /* 已标记,幂等 */
if (!vma_ksm_compatible(vma))
return 0; /* 不兼容的 VMA 类型,静默忽略 */
if (!mm_flags_test(MMF_VM_MERGEABLE, mm)) {
err = __ksm_enter(mm); /* 首次参与,注册到扫描链表 */
if (err)
return err;
}
*vm_flags |= VM_MERGEABLE; /* 设置 VMA 标志 */
break;
case MADV_UNMERGEABLE:
if (!(*vm_flags & VM_MERGEABLE))
return 0; /* 未标记,忽略 */
if (vma->anon_vma) {
err = break_ksm(vma, start, end, true); /* 解除已合并的页 */
if (err)
return err;
}
*vm_flags &= ~VM_MERGEABLE; /* 清除 VMA 标志 */
break;
}
return 0;
}VMA 兼容性检查(mm/ksm.c:738-756,ksm_compatible()):
static bool ksm_compatible(const struct file *file, vm_flags_t vm_flags)
{
/* 以下类型 VMA 不参与 KSM */
if (vm_flags & (VM_SHARED | VM_MAYSHARE | VM_SPECIAL |
VM_HUGETLB | VM_DROPPABLE))
return false;
if (file_is_dax(file)) /* DAX 直接访问映射 */
return false;
#ifdef VM_SAO /* PowerPC 强排序属性 */
if (vm_flags & VM_SAO)
return false;
#endif
#ifdef VM_SPARC_ADI /* SPARC ADI 属性 */
if (vm_flags & VM_SPARC_ADI)
return false;
#endif
return true;
}int __ksm_enter(struct mm_struct *mm)
{
struct ksm_mm_slot *mm_slot;
mm_slot = mm_slot_alloc(mm_slot_cache); /* 从 slab 缓存分配 */
if (!mm_slot) return -ENOMEM;
needs_wakeup = list_empty(&ksm_mm_head.slot.mm_node); /* 链表是否为空 */
spin_lock(&ksm_mmlist_lock);
mm_slot_insert(mm_slots_hash, mm, slot); /* 插入哈希表 */
/* 插入位置策略:
* KSM_RUN_UNMERGE 模式:插入链表尾部(确保解除合并时不漏掉)
* 其他模式:插入扫描游标之后(让新注册的 mm 先"沉淀"一段时间)
*/
if (ksm_run & KSM_RUN_UNMERGE)
list_add_tail(&slot->mm_node, &ksm_mm_head.slot.mm_node);
else
list_add_tail(&slot->mm_node, &ksm_scan.mm_slot->slot.mm_node);
spin_unlock(&ksm_mmlist_lock);
mm_flags_set(MMF_VM_MERGEABLE, mm);
mmgrab(mm); /* 增加 mm 引用计数,防止在 ksmd 扫描前被释放 */
if (needs_wakeup)
wake_up_interruptible(&ksm_thread_wait); /* 唤醒 ksmd */
trace_ksm_enter(mm);
return 0;
}注意插入游标之后(而非链表头部)的设计:当 fork 后立即 exec 时,ksmd 还没来得及扫描就已经退出,避免了 ksmd 为即将消亡的 mm 建立 rmap_item 的浪费。
当进程设置了 MMF_VM_MERGE_ANY 后,每次新建 VMA 时内核自动检查是否应加入 KSM:
/* mm/ksm.c:2867-2879 */
vm_flags_t ksm_vma_flags(struct mm_struct *mm, const struct file *file,
vm_flags_t vm_flags)
{
if (mm_flags_test(MMF_VM_MERGE_ANY, mm) &&
__ksm_should_add_vma(file, vm_flags)) {
vm_flags |= VM_MERGEABLE;
/*
* Generally, the flags here always include MMF_VM_MERGEABLE.
* However, in rare cases, this flag may be cleared by ksmd who
* scans a cycle without finding any mergeable vma.
*/
if (unlikely(!mm_flags_test(MMF_VM_MERGEABLE, mm)))
__ksm_enter(mm);
}
return vm_flags;
}这意味着 mmap(MAP_ANONYMOUS) 返回的新区域会自动带上 VM_MERGEABLE 标志,无需应用程序逐个 madvise()。
在 Kubernetes/Docker 等容器场景中,多个运行相同基础镜像的容器会产生大量相同内存页:
基础镜像(ubuntu:22.04)运行 10 个容器:
每个容器进程(PID namespace):
│
├─ /lib/x86_64-linux-gnu/libc.so.6(~2MB 代码页)
├─ /usr/bin/python3(如果相同版本)
└─ 相同的环境变量、配置等只读数据
不使用 KSM:10 * 2MB = 20MB 物理内存用于 libc 代码
使用 KSM:~2MB 物理内存(合并后共享)+ 元数据开销
节省:~18MB(90%)
实际节省取决于容器是否有写操作(写操作触发 COW 解除共享)
ksm_fork 钩子(include/linux/ksm.h:56-66):
static inline void ksm_fork(struct mm_struct *mm, struct mm_struct *oldmm)
{
/* Adding mm to ksm is best effort on fork. */
if (mm_flags_test(MMF_VM_MERGEABLE, oldmm)) {
long nr_ksm_zero_pages = atomic_long_read(&mm->ksm_zero_pages);
mm->ksm_merging_pages = 0;
mm->ksm_rmap_items = 0;
atomic_long_add(nr_ksm_zero_pages, &ksm_zero_pages);
__ksm_enter(mm); /* 子进程自动继承父进程的 KSM 参与状态 */
}
}fork 时子进程自动加入 KSM 扫描链表,这正是容器运行时(containerd、runc)启动容器时零页合并的基础。
透明大页(Transparent Hugepage,THP)使用 2MB(x86_64)的 PMD 级大页,而 KSM 只处理基本的 4KB 页面。KSM 在尝试合并大页时必须先将其拆分:
/* mm/ksm.c:1497-1501 */
if (folio_test_large(folio)) {
if (split_huge_page(page))
goto out_unlock; /* 拆分失败则放弃本次合并 */
folio = page_folio(page);
}拆分可能失败的情况:
- 页被锁定(
folio_lock(folio)竞争) - 引用计数太高(其他代码持有引用)
- 页处于 swap 路径中
拆分失败时,ksmd 放弃本次合并机会,等待下一轮扫描时重试。
madvise(MADV_HUGEPAGE) 区域 madvise(MADV_MERGEABLE) 区域
│ │
│ 缺页时分配 2MB 大页 │ ksmd 扫描
▼ ▼
[PMD 级 2MB THP] [普通 4KB 页]
│ │
│ ksmd 发现此 THP(配置了 MERGEABLE) │
│ folio_test_large(folio) == true │
│ │
▼ │
split_huge_page() │
│ │
成功 │ 失败(返回) │
│ │
▼ │
[512 个 4KB 普通页] ──→ 进入 KSM 扫描 ←─────────┘
│
▼
cmp_and_merge_page()
(正常 4KB 合并流程)
性能影响:
拆分 THP 有显著性能开销:
- 需要分配 512 个
struct page的元数据(通常已存在,但需要更新) - TLB 从 PMD 级 mapping 变为 512 个 PTE,增加 TLB 压力
- 拆分后失去 THP 的内存访问性能优势
因此在内存充裕的场景下,若应用对大页性能敏感(如数据库、JVM),不建议同时使用 MADV_HUGEPAGE 和 MADV_MERGEABLE。
struct folio *ksm_might_need_to_copy(struct folio *folio,
struct vm_area_struct *vma, unsigned long addr)
{
/* 大页直接返回,KSM 不处理(复制大页开销太大)*/
if (folio_test_large(folio))
return folio;
...
}当大页从 swap 换入时,ksm_might_need_to_copy() 对大页不做任何处理(直接返回原 folio),避免了 2MB 页面的复制开销。大页的 KSM 状态需要等到被 THP 拆分后才能重新评估。
khugepaged(THP 聚合守护线程)与 ksmd 存在潜在冲突:
khugepaged 路径:
尝试将相邻 4KB 页合并为 2MB THP
→ 若区域内有 KSM 页(写保护)→ 需要先 break_ksm
→ 或 collapse_huge_page() 跳过 KSM 页
ksmd 路径:
尝试将 2MB THP 拆分为 4KB 页
→ 然后逐个 4KB 页做 KSM 合并
这两个守护线程可能形成"拆了合,合了拆"的循环
→ 内核通过 VMA 标志冲突避免:
VM_HUGEPAGE(khugepaged 偏好)与 VM_MERGEABLE(ksmd 偏好)可共存
但在页级别,二者是互斥的状态
最佳实践:对于 KSM 目标区域(如匿名数据段),建议通过 madvise(MADV_NOHUGEPAGE) 禁用 THP,避免两个守护线程相互干扰。
普通匿名页通过 anon_vma 链进行反向映射(从物理页找到所有映射它的 VMA 和 PTE)。KSM 页的映射横跨多个不相关进程(无父子关系),因此需要专门的 rmap 实现。
void rmap_walk_ksm(struct folio *folio, struct rmap_walk_control *rwc)
{
struct ksm_stable_node *stable_node;
struct ksm_rmap_item *rmap_item;
int search_new_forks = 0;
VM_BUG_ON_FOLIO(!folio_test_ksm(folio), folio);
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio); /* 必须持有页锁 */
stable_node = folio_stable_node(folio);
if (!stable_node) return;
again:
hlist_for_each_entry(rmap_item, &stable_node->hlist, hlist) {
struct anon_vma *anon_vma = rmap_item->anon_vma;
struct anon_vma_chain *vmac;
struct vm_area_struct *vma;
cond_resched();
if (!anon_vma_trylock_read(anon_vma)) {
if (rwc->try_lock) {
rwc->contended = true;
return;
}
anon_vma_lock_read(anon_vma);
}
anon_vma_interval_tree_foreach(vmac, &anon_vma->rb_root, 0, ULONG_MAX) {
vma = vmac->vma;
addr = rmap_item->address & PAGE_MASK;
if (addr < vma->vm_start || addr >= vma->vm_end) continue;
if ((rmap_item->mm == vma->vm_mm) == search_new_forks) continue;
if (!rwc->rmap_one(folio, vma, addr, rwc->arg)) { ... return; }
}
anon_vma_unlock_read(anon_vma);
}
if (!search_new_forks++) goto again; /* 第二轮:处理 fork 后的新映射 */
}两轮遍历的必要性:
- 第一轮(
search_new_forks=0):只遍历rmap_item->mm == vma->vm_mm的 VMA(原始映射进程) - 第二轮(
search_new_forks=1):遍历rmap_item->mm != vma->vm_mm的 VMA(fork 后继承了此 KSM 页的子进程)
这个两轮设计处理了以下场景:当进程 A 映射了 KSM 页后,进程 A fork 出子进程 B,子进程 B 通过 COW 继承了该 KSM 页的映射。此时 rmap_item->mm == mm_A,但 vma->vm_mm 可能是 mm_B(fork 后的新 anon_vma 包含子进程的 VMA)。
static void stable_tree_append(struct ksm_rmap_item *rmap_item,
struct ksm_stable_node *stable_node,
bool max_page_sharing_bypass)
{
BUG_ON(stable_node->rmap_hlist_len < 0); /* 检测内存损坏 */
stable_node->rmap_hlist_len++;
if (!max_page_sharing_bypass)
WARN_ON_ONCE(stable_node->rmap_hlist_len > ksm_max_page_sharing);
rmap_item->head = stable_node;
rmap_item->address |= STABLE_FLAG;
hlist_add_head(&rmap_item->hlist, &stable_node->hlist);
if (rmap_item->hlist.next)
ksm_pages_sharing++; /* 非第一个映射:sharing++ */
else
ksm_pages_shared++; /* 第一个映射(共享页本身):shared++ */
rmap_item->mm->ksm_merging_pages++; /* 进程级计数 */
}void folio_migrate_ksm(struct folio *newfolio, struct folio *folio)
{
struct ksm_stable_node *stable_node;
VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio);
VM_BUG_ON_FOLIO(!folio_test_locked(newfolio), newfolio);
stable_node = folio_stable_node(folio);
if (stable_node) {
stable_node->kpfn = folio_pfn(newfolio);
smp_wmb(); /* 确保 kpfn 更新在 mapping 清空之前可见 */
folio_set_stable_node(folio, NULL); /* 解除旧 folio 与 stable_node 关联 */
}
}smp_wmb() 对应 ksm_get_folio() 中的 smp_rmb()(mm/ksm.c:1015),保证 ksm_get_folio() 在检测到 stale 状态(folio->mapping 不匹配)后,能够正确读到更新后的 kpfn 值,区分"页已迁移(应重试)"和"页已释放(应清理)"两种情况。
这是一个精妙的无锁设计,使稳定节点不需要持有页引用计数:
static struct folio *ksm_get_folio(struct ksm_stable_node *stable_node,
enum ksm_get_folio_flags flags)
{
struct folio *folio;
void *expected_mapping;
unsigned long kpfn;
expected_mapping = (void *)((unsigned long)stable_node | FOLIO_MAPPING_KSM);
again:
kpfn = READ_ONCE(stable_node->kpfn); /* 地址依赖读 */
folio = pfn_folio(kpfn);
if (READ_ONCE(folio->mapping) != expected_mapping)
goto stale; /* mapping 不匹配:页已释放或迁移 */
/* 原子性增加引用计数 */
while (!folio_try_get(folio)) {
/* 引用计数为 0:可能正在 page_ref_freeze()(页回收路径)*/
if (!folio_test_swapcache(folio))
goto stale; /* 非 swapcache → 页确实正在被释放 */
cpu_relax(); /* swapcache → 可能是迁移中,自旋等待 */
}
if (READ_ONCE(folio->mapping) != expected_mapping) {
folio_put(folio); /* 引用计数获取后 mapping 变了:放弃并重检 */
goto stale;
}
return folio; /* 成功:folio 有效且持有引用 */
stale:
smp_rmb(); /* 对应 folio_migrate_ksm() 中的 smp_wmb() */
if (READ_ONCE(stable_node->kpfn) != kpfn)
goto again; /* kpfn 变化:页在迁移中,重试 */
remove_node_from_stable_tree(stable_node); /* kpfn 未变但 mapping 不对:节点失效 */
return NULL;
}为何不持有页引用:
若稳定节点持有页引用计数,会阻止 kswapd 将 KSM 页换出(页引用计数 > 0 时不能换出)。这会导致 KSM 页永久占据物理内存,丧失了与 swap 机制协作的能力。keyhole 机制通过 folio->mapping 字段存储反向指针(而非增加引用计数),避免了这一问题。
/* mm/ksm.c:3373-3413 */
static int ksm_memory_callback(struct notifier_block *self,
unsigned long action, void *arg)
{
struct memory_notify *mn = arg;
switch (action) {
case MEM_GOING_OFFLINE:
/* 设置 KSM_RUN_OFFLINE 标志,阻止 ksmd 访问稳定树 */
ksm_run |= KSM_RUN_OFFLINE;
break;
case MEM_OFFLINE:
/* 清理 kpfn 落在下线内存范围内的 stable_node */
ksm_check_stable_tree(mn->start_pfn, mn->start_pfn + mn->nr_pages);
fallthrough;
case MEM_CANCEL_OFFLINE:
/* 清除 OFFLINE 标志,唤醒 ksmd */
ksm_run &= ~KSM_RUN_OFFLINE;
wake_up_bit(&ksm_run, ilog2(KSM_RUN_OFFLINE));
break;
}
return NOTIFY_OK;
}wait_while_offlining() 在 ksmd 主循环中调用(mm/ksm.c:2810),当 KSM_RUN_OFFLINE 标志设置时阻塞:
static void wait_while_offlining(void)
{
while (ksm_run & KSM_RUN_OFFLINE) {
mutex_unlock(&ksm_thread_mutex);
wait_on_bit(&ksm_run, ilog2(KSM_RUN_OFFLINE),
TASK_UNINTERRUPTIBLE);
mutex_lock(&ksm_thread_mutex);
}
}ksm_check_stable_tree() 遍历所有稳定树节点(mm/ksm.c:3344-3371),删除 kpfn 落在下线 PFN 范围内的节点:
PFN 范围 [start_pfn, start_pfn + nr_pages)
稳定树遍历:
for each ksm_stable_node in rb_tree:
if is_chain(stable_node):
for each dup in chain:
if dup->kpfn >= start_pfn && dup->kpfn < end_pfn:
remove_node_from_stable_tree(dup)
else:
if stable_node->kpfn >= start_pfn && < end_pfn:
remove_node_from_stable_tree(stable_node)
由于稳定节点不持有页引用(keyhole 机制),可以安全地通过 PFN 范围判断并直接删除,无需处理引用计数归零等问题。
所有接口位于 /sys/kernel/mm/ksm/:
参数 默认值 类型 说明
────────────────────────────────────────────────────────────────────────────
run 0 RW 0=停止, 1=合并, 2=解除合并
pages_to_scan 100 RW 每批扫描页数(advisor=none 时有效)
sleep_millisecs 20 RW 批次间隔毫秒数
max_page_sharing 256 RW 每个稳定节点最大映射数
merge_across_nodes(NUMA) 1 RW 1=跨节点合并, 0=仅同节点
use_zero_pages 0 RW 1=零页特殊优化
smart_scan 1 RW 1=启用跳过机制
stable_node_chains_prune_millisecs 2000 RW chain 节点垃圾回收间隔(ms)
advisor_mode none RW none 或 scan-time
advisor_max_cpu 70 RW advisor 最大 CPU 使用率(%)
advisor_min_pages_to_scan 500 RW advisor 最小批次大小
advisor_max_pages_to_scan 30000 RW advisor 最大批次大小
advisor_target_scan_time 200 RW advisor 目标扫描时间(秒)
统计指标 含义
────────────────────────────────────────────────────────────────────────
pages_shared 稳定树中的 KSM 节点数(独立物理共享页数)
pages_sharing 额外共享此节点的映射数(节省 = sharing * PAGE_SIZE)
pages_unshared 不稳定树节点数(等待匹配的候选页数)
pages_volatile 已跟踪但内容频繁变化的页数
pages_skipped smart_scan 跳过的页数(累计)
ksm_zero_pages KSM 放置的零页总数
full_scans 完整扫描轮次(seqnr)
pages_scanned 总扫描页数(累计)
general_profit 整体净内存节省(字节,可能为负)
stable_node_chains chain 节点数量
stable_node_dups dup 节点数量
echo 0 echo 1
STOP ─────────────> STOP STOP ─────────────> MERGE
MERGE ────────────> STOP MERGE ─────────────> MERGE(无操作)
UNMERGE ──────────> 不允许 先 echo 2,再 echo 1 进入 UNMERGE→MERGE 序列
echo 2
任何状态 ──────────> UNMERGE
(解除所有合并,保留 mm_slot 列表)
KSM_RUN_UNMERGE 时:
unmerge_and_remove_all_rmap_items()
→ 对所有 VM_MERGEABLE VMA 执行 break_ksm()
→ 所有进程的 KSM 页恢复为私有匿名页
# 1. 启用 KSM(默认停止状态)
echo 1 > /sys/kernel/mm/ksm/run
# 2. 使能自动调速(推荐生产环境)
echo scan-time > /sys/kernel/mm/ksm/advisor_mode
# 3. 启用零页优化(适合大量匿名内存的场景)
echo 1 > /sys/kernel/mm/ksm/use_zero_pages
# 4. 验证运行效果
cat /sys/kernel/mm/ksm/pages_shared # 已合并的独立 KSM 页数
cat /sys/kernel/mm/ksm/pages_sharing # 通过 KSM 节省的映射数
cat /sys/kernel/mm/ksm/general_profit # 净节省字节数
# 5. 计算节省的实际内存
PAGES_SHARING=$(cat /sys/kernel/mm/ksm/pages_sharing)
ZERO_PAGES=$(cat /sys/kernel/mm/ksm/ksm_zero_pages)
echo "KSM 节省内存:$(( (PAGES_SHARING + ZERO_PAGES) * 4 )) KB"KSM 元数据内存开销(x86_64):
ksm_rmap_item:约 40 字节 × ksm_rmap_items(追踪的页总数)
ksm_stable_node:约 48 字节 × (ksm_pages_shared + ksm_stable_node_dups + ksm_stable_node_chains)
ksm_mm_slot:约 32 字节 × 参与 KSM 的进程数
mm_slots_hash:固定 8KB(1024 × 8 字节)
示例(100 万候选页,10 万已合并):
rmap_item 开销 = 1,000,000 × 40B = 40 MB
stable_node 开销 = 100,000 × 48B = 4.8 MB
总开销 ≈ 45 MB
pages_sharing(假设平均 3 个进程共享)= 200,000 个
节省内存 = 200,000 × 4KB = 800 MB
净收益 = 800 MB - 45 MB = 755 MB(约 94% 的 ROI)
在极端情况下(大量候选页但合并率极低),rmap_item 的累积开销可能超过合并收益:
当 general_profit < 0 时:
→ 减少 MADV_MERGEABLE 的区域范围
→ 提高 pages_to_scan(加快合并,减少 rmap_item 滞留时间)
→ 或直接关闭某些不活跃进程的 KSM 参与
默认配置(100 页/批,20ms 间隔):
扫描速率 = 100 / 0.020s = 5,000 页/秒 = 约 20 MB/秒
扫描 1GB(256K 页)耗时 ≈ 51 秒(约 2 个 full_scan)
推荐虚拟化主机配置(加快合并):
pages_to_scan = 1000,sleep_millisecs = 10
扫描速率 = 100,000 页/秒 = 400 MB/秒
扫描 1GB 耗时 ≈ 2.5 秒
CPU 开销:在 20 核服务器上约 1-3%(主要是内存访问延迟)
最优场景:
场景 典型节省率 说明
──────────────────────────────────────────────────────────────────
KVM 多 guest 50-80% 相同 OS 内核、libc 大量重复
Kubernetes 容器 30-60% 相同基础镜像的多个 Pod
Java 应用集群 20-40% JVM 运行时类元数据重复
Redis 多实例 40-70% 相同的数据结构内存布局
不适用场景:
- 实时系统(PREEMPT_RT):COW 触发的缺页延迟不可预测
- 安全隔离要求高的多租户:侧信道攻击风险
- 内存访问模式高度随机:扫描开销 > 合并收益
- 已使用 THP 的工作负载(频繁 split/merge 循环)
KSM 本身不直接支持 cgroup 配额,但提供了进程级统计基础:
per-mm 统计(通过 /proc/<pid>/ksm_stat):
mm->ksm_merging_pages:此进程通过 KSM 节省的页数
mm->ksm_rmap_items:此进程的 rmap_item 数
mm->ksm_zero_pages:此进程的零页计数
ksm_process_profit(mm):进程级净收益(字节)
cgroup v2 内存控制器(需内核支持):
memory.stat 中包含 ksm_pages_shared、ksm_pages_sharing 字段
mem_cgroup_charge() 在 ksm_might_need_to_copy() 中调用(mm/ksm.c:3131)
确保 KSM COW 复制出的新页正确计入 cgroup 内存配额
#!/bin/bash
# KSM 效率监控
while true; do
SHARED=$(cat /sys/kernel/mm/ksm/pages_shared)
SHARING=$(cat /sys/kernel/mm/ksm/pages_sharing)
PROFIT=$(cat /sys/kernel/mm/ksm/general_profit)
SCANS=$(cat /sys/kernel/mm/ksm/full_scans)
SAVED_MB=$(( SHARING * 4 / 1024 ))
PROFIT_MB=$(( PROFIT / 1024 / 1024 ))
echo "$(date '+%H:%M:%S') scans=$SCANS shared=$SHARED sharing=$SHARING saved=${SAVED_MB}MB profit=${PROFIT_MB}MB"
sleep 10
done用户调用 madvise(MADV_MERGEABLE)
→ sys_madvise() (mm/madvise.c)
→ madvise_vma_behavior()
→ ksm_madvise(vma, start, end, MADV_MERGEABLE, &vm_flags) (mm/ksm.c:2975)
→ vma_ksm_compatible(vma) 兼容性检查
→ __ksm_enter(mm) (mm/ksm.c:3015) 首次参与时调用
→ mm_slot_alloc() 分配 ksm_mm_slot
→ mm_slot_insert(mm_slots_hash, mm) 插入哈希表
→ list_add_tail(&slot->mm_node, ...) 插入扫描链表
→ mm_flags_set(MMF_VM_MERGEABLE, mm) 设置 mm 标志
→ mmgrab(mm) 增加 mm 引用计数
→ wake_up_interruptible(&ksm_thread_wait) 唤醒 ksmd
→ *vm_flags |= VM_MERGEABLE 设置 VMA 标志
或 prctl(PR_SET_MEMORY_MERGE, 1)
→ ksm_enable_merge_any(mm) (mm/ksm.c:2916)
→ __ksm_enter(mm)
→ mm_flags_set(MMF_VM_MERGE_ANY, mm)
→ ksm_add_vmas(mm) 对所有兼容 VMA 设置 VM_MERGEABLE
ksmd: ksm_scan_thread() (mm/ksm.c:2801)
└─ ksm_do_scan(pages_to_scan) (mm/ksm.c:2780)
└─ scan_get_next_rmap_item() (mm/ksm.c:2574)
├─ [新轮次] 清空不稳定树
├─ walk_page_range_vma() 遍历 VMA 找匿名页
├─ get_next_rmap_item() 创建/复用 rmap_item
└─ should_skip_rmap_item() smart_scan 过滤
└─ cmp_and_merge_page() (mm/ksm.c:2248)
├─ calc_checksum(xxhash32) 计算页校验和
├─ [checksum 未变] 继续
├─ try_to_merge_with_zero_page() 零页优化路径
├─ stable_tree_search() (mm/ksm.c:1825)
│ └─ ksm_get_folio() keyhole 引用验证
│ └─ memcmp_pages() 内容比较
├─ try_to_merge_with_ksm_page() (mm/ksm.c:1579)
│ └─ try_to_merge_one_page() (mm/ksm.c:1475)
│ ├─ folio_trylock()
│ ├─ split_huge_page() 大页时先拆分
│ ├─ write_protect_page() (mm/ksm.c:1272)
│ │ ├─ ptep_clear_flush() 清除 PTE,刷 TLB
│ │ └─ pte_wrprotect() 设写保护
│ └─ replace_page() (mm/ksm.c:1372)
│ ├─ ptep_clear_flush()
│ └─ set_pte_at(newpte) PTE 指向 KSM 页
├─ stable_tree_append() (mm/ksm.c:2205)
└─ unstable_tree_search_insert() (mm/ksm.c:2133)
└─ [命中] try_to_merge_two_pages() (mm/ksm.c:1618)
└─ stable_tree_insert() (mm/ksm.c:2039)
进程写入 KSM 页(写保护触发缺页)
→ handle_mm_fault() (mm/memory.c)
→ do_wp_page()
→ wp_page_copy() 分配新页并复制 KSM 页内容
→ set_pte_at(new_pte) 新 PTE 可写,指向新私有页
→ folio_remove_rmap_pte(kfolio) 从 KSM 页移除 rmap 记录
或显式解除合并:
madvise(MADV_UNMERGEABLE)
→ ksm_madvise() → break_ksm() (mm/ksm.c:692)
→ handle_mm_fault(FAULT_FLAG_UNSHARE) 触发 COW 私有化
→ 所有 KSM 页恢复为独立匿名页
或 KSM_RUN_UNMERGE(echo 2 > /sys/kernel/mm/ksm/run):
→ unmerge_and_remove_all_rmap_items()
→ 遍历所有 mm_slot,对所有 VM_MERGEABLE VMA 调用 break_ksm()
| 功能 | 文件 | 行号 |
|---|---|---|
ksm_mm_slot 结构定义 |
mm/ksm.c |
126 |
ksm_scan 结构定义 |
mm/ksm.c |
140 |
ksm_stable_node 结构定义 |
mm/ksm.c |
159 |
ksm_rmap_item 结构定义 |
mm/ksm.c |
201 |
SEQNR_MASK/UNSTABLE_FLAG/STABLE_FLAG |
mm/ksm.c |
223 |
| 稳定树/不稳定树根节点 | mm/ksm.c |
228 |
migrate_nodes 链表 |
mm/ksm.c |
234 |
mm_slots_hash 哈希表 |
mm/ksm.c |
237 |
| slab 缓存指针 | mm/ksm.c |
247 |
DEFAULT_PAGES_TO_SCAN |
mm/ksm.c |
252 |
ksm_pages_shared/sharing 变量 |
mm/ksm.c |
258 |
advisor_ctx 结构定义 |
mm/ksm.c |
325 |
ewma() 函数 |
mm/ksm.c |
376 |
scan_time_advisor() |
mm/ksm.c |
402 |
KSM_RUN_STOP/MERGE/UNMERGE/OFFLINE |
mm/ksm.c |
478 |
ksm_slab_init() |
mm/ksm.c |
490 |
break_ksm() |
mm/ksm.c |
692 |
ksm_compatible() |
mm/ksm.c |
738 |
ksm_get_folio() |
mm/ksm.c |
946 |
write_protect_page() |
mm/ksm.c |
1272 |
replace_page() |
mm/ksm.c |
1372 |
try_to_merge_one_page() |
mm/ksm.c |
1475 |
try_to_merge_with_zero_page() |
mm/ksm.c |
1539 |
try_to_merge_with_ksm_page() |
mm/ksm.c |
1579 |
try_to_merge_two_pages() |
mm/ksm.c |
1618 |
stable_tree_search() |
mm/ksm.c |
1825 |
stable_tree_insert() |
mm/ksm.c |
2039 |
stable_tree_append() |
mm/ksm.c |
2205 |
unstable_tree_search_insert() |
mm/ksm.c |
2133 |
cmp_and_merge_page() |
mm/ksm.c |
2248 |
skip_age() |
mm/ksm.c |
2424 |
should_skip_rmap_item() |
mm/ksm.c |
2442 |
scan_get_next_rmap_item() |
mm/ksm.c |
2574 |
ksm_do_scan() |
mm/ksm.c |
2780 |
ksmd_should_run() |
mm/ksm.c |
2796 |
ksm_scan_thread() |
mm/ksm.c |
2801 |
ksm_vma_flags() |
mm/ksm.c |
2867 |
ksm_enable_merge_any() |
mm/ksm.c |
2916 |
ksm_disable_merge_any() |
mm/ksm.c |
2947 |
ksm_madvise() |
mm/ksm.c |
2975 |
__ksm_enter() |
mm/ksm.c |
3015 |
__ksm_exit() |
mm/ksm.c |
3058 |
ksm_might_need_to_copy() |
mm/ksm.c |
3104 |
rmap_walk_ksm() |
mm/ksm.c |
3152 |
folio_migrate_ksm() |
mm/ksm.c |
3267(约) |
ksm_process_profit() |
mm/ksm.c |
3440 |
pages_shared_show() |
mm/ksm.c |
3682 |
pages_volatile_show() |
mm/ksm.c |
3703 |
general_profit_show() |
mm/ksm.c |
3734 |
ksm_attrs[] sysfs 属性列表 |
mm/ksm.c |
3939 |
ksm_init() 初始化函数 |
mm/ksm.c |
3975 |
| KSM 公共接口声明 | include/linux/ksm.h |
17 |
is_ksm_zero_pte() 宏 |
include/linux/ksm.h |
33 |
ksm_fork() 内联函数 |
include/linux/ksm.h |
56 |
ksm_might_need_to_copy() 声明 |
include/linux/ksm.h |
94 |
ksm_might_need_to_copy() 调用(swap) |
mm/swapfile.c |
约 2098 |
ksm_might_need_to_copy() 调用(fault) |
mm/memory.c |
约 4854 |
由 Claude Code 分析生成