3. KSM 内核架构:位置、数据结构与初始化流程
KSM(Kernel Same-page Merging)是 Linux 内核中一个轻量级的内存去重子系统,位于 mm/ksm.c 源文件中。它通过后台内核线程扫描用户态进程的匿名内存页,识别并合并内容完全相同的物理页面,从而减少物理内存占用。理解 KSM 在内核中的位置、核心数据结构以及模块初始化流程,是掌握其工程落地原理的基础。
3.1 KSM 在内核中的位置
KSM 属于内存管理子系统(mm)的一部分,与页面回收(page reclaim)、透明大页(THP)、内存规整(compaction)等机制协同工作。其调用关系与依赖层次如下表所示:
| 层次 | 模块/子系统 | 与 KSM 的关系 |
|---|---|---|
| 应用层 | 用户态进程(如 QEMU/KVM) | 通过 madvise(MADV_MERGEABLE) 标记可合并内存区域 |
| 系统调用层 | sys_madvise / sys_ksm |
接收用户态指令,调用 KSM 核心接口 |
| 内存管理核心 | mm/ksm.c | 扫描、比较、合并、回退逻辑 |
| 页表与反向映射 | rmap.c / page_vma_mapped_walk | KSM 通过反向映射更新页表项 |
| 物理页分配器 | buddy / slab | 释放合并后的冗余页面 |
关键点: KSM 不直接操作硬件页表,而是通过标准的内存管理接口(如
page_add_ksm_rmap、try_to_unmap)完成页表更新与页面回收,因此对内核其他模块的侵入性较低。
3.2 关键数据结构
KSM 使用两棵红黑树(stable tree 和 unstable tree)来组织已合并和待扫描的页面。核心数据结构包括 ksm_mm_slot 和 ksm_stable_node,它们分别管理进程粒度和页面粒度的去重状态。
3.2.1 ksm_mm_slot:进程级管理结构
每个调用 madvise(MADV_MERGEABLE) 的进程(mm_struct)都会对应一个 ksm_mm_slot。它负责维护该进程内所有可合并虚拟内存区域(VMA)的扫描状态。
struct ksm_mm_slot {
struct mm_struct *mm; /* 指向进程的内存描述符 */
struct ksm_scan scan; /* 当前扫描位置(地址、索引) */
struct list_head mm_list; /* 全局 ksm_mm_head 链表节点 */
struct rcu_head rcu; /* RCU 回调,用于安全释放 */
};
| 字段 | 类型 | 作用 |
|---|---|---|
mm |
struct mm_struct * |
关联的进程内存描述符,用于获取页表与 VMA |
scan |
struct ksm_scan |
记录当前扫描到的地址、页号、VMA 偏移,支持断点续扫 |
mm_list |
struct list_head |
链接到全局链表,供 ksmd 内核线程遍历 |
工程提示:
ksm_scan 结构体包含 address、rmap_item 等字段,使得 KSM 在每次唤醒时可以从上次中断的位置继续扫描,避免重复遍历已检查的页面。
3.2.2 ksm_stable_node:页面级稳定节点
当两个页面内容完全一致且被合并后,KSM 会在 stable tree 中插入一个 ksm_stable_node。该节点代表一个“稳定的”合并页面,所有映射到该物理页面的虚拟页都通过此节点管理。
struct ksm_stable_node {
struct rb_node node; /* 红黑树节点,按页面内容哈希值排序 */
struct page *page; /* 指向合并后的物理页面(写保护) */
struct list_head list; /* 链接到该页面所有 rmap_item 的链表 */
unsigned long kpfn; /* 页面帧号,用于快速查找 */
unsigned int ksm_flags; /* 状态标志(如合并中、回退中) */
};
| 字段 | 类型 | 作用 |
|---|---|---|
node |
struct rb_node |
红黑树节点,按内容哈希值组织,支持 O(log n) 查找 |
page |
struct page * |
合并后的唯一物理页面,被标记为 PG_ksm |
list |
struct list_head |
链接所有映射到此页面的 ksm_rmap_item,用于反向映射 |
kpfn |
unsigned long |
物理页帧号,用于快速定位页面 |
3.3 模块初始化流程
KSM 模块的初始化分为两个阶段:编译时静态初始化 和 运行时动态初始化。以下流程基于 Linux 5.10 内核源码分析。
- ksm_init() 入口:在
mm/ksm.c中通过module_init(ksm_init)注册。该函数在系统启动或模块加载时调用。 - 创建 ksmd 内核线程:调用
kthread_run(ksm_scan_thread, NULL, "ksmd")创建后台扫描线程。该线程负责周期性扫描可合并页面。 - 初始化全局链表与锁:初始化
ksm_mm_head链表(用于管理所有ksm_mm_slot)以及ksm_thread_mutex互斥锁。 - 注册 sysfs 接口:通过
kernel_subsys创建/sys/kernel/mm/ksm/目录,暴露run、sleep_millisecs、pages_to_scan等控制参数。 - 设置默认参数:
ksm_run默认设为KSM_RUN_STOP(0),等待用户态写入1启动合并;ksm_sleep_millisecs默认 20 毫秒。
static int __init ksm_init(void)
{
struct task_struct *thread;
int err;
/* 1. 初始化全局链表与锁 */
INIT_LIST_HEAD(&ksm_mm_head);
spin_lock_init(&ksm_mmlist_lock);
mutex_init(&ksm_thread_mutex);
/* 2. 创建 ksmd 内核线程 */
thread = kthread_run(ksm_scan_thread, NULL, "ksmd");
if (IS_ERR(thread)) {
pr_err("ksm: creating kthread failed\n");
return PTR_ERR(thread);
}
ksmd_thread = thread;
/* 3. 注册 sysfs 接口 */
err = ksm_sysfs_init();
if (err) {
kthread_stop(thread);
return err;
}
/* 4. 设置默认运行状态为停止 */
ksm_run = KSM_RUN_STOP;
pr_info("KSM initialized\n");
return 0;
}
注意: 即使 KSM 模块初始化成功,默认也不会立即开始扫描。用户必须通过
echo 1 > /sys/kernel/mm/ksm/run 显式启动合并操作。这是为了避免在不需要去重的场景下浪费 CPU 资源。
3.4 初始化后的状态与用户接口
初始化完成后,KSM 处于“待命”状态。用户可以通过 sysfs 接口控制其行为。下表列出了最关键的三个控制参数:
| sysfs 文件 | 类型 | 默认值 | 作用 |
|---|---|---|---|
/sys/kernel/mm/ksm/run |
unsigned int | 0 | 0=停止,1=启动合并,2=仅取消合并(不合并新页) |
/sys/kernel/mm/ksm/sleep_millisecs |
unsigned long | 20 | ksmd 两次扫描之间的休眠时间(毫秒) |
/sys/kernel/mm/ksm/pages_to_scan |
unsigned long | 100 | 每次唤醒扫描的页面数量 |
最佳实践: 在虚拟机场景中,建议将
sleep_millisecs 调整为 100~200 毫秒,pages_to_scan 调整为 200~500,以平衡去重效率与 CPU 开销。对于延迟敏感型负载,应谨慎启用 KSM。