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)完成页表更新与页面回收,因此对内核其他模块的侵入性较低。
KSM 在内核中的位置与调用关系 用户态进程 (QEMU/KVM) sys_madvise / sys_ksm KSM 核心 (mm/ksm.c) 反向映射 (rmap.c) Buddy / Slab 分配器 说明 KSM 通过标准内存管理 接口操作页表与页面, 不直接访问硬件。

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 物理页帧号,用于快速定位页面
ksm_stable_node 与 rmap_item 的关系 ksm_stable_node page: 物理页 A kpfn: 0x12345 ksm_rmap_item vma: 进程 P1 ksm_rmap_item vma: 进程 P2 ksm_rmap_item vma: 进程 P3 一个稳定物理页可被多个进程的 rmap_item 共享,通过 list 串联

3.3 模块初始化流程

KSM 模块的初始化分为两个阶段:编译时静态初始化 和 运行时动态初始化。以下流程基于 Linux 5.10 内核源码分析。

  1. ksm_init() 入口:在 mm/ksm.c 中通过 module_init(ksm_init) 注册。该函数在系统启动或模块加载时调用。
  2. 创建 ksmd 内核线程:调用 kthread_run(ksm_scan_thread, NULL, "ksmd") 创建后台扫描线程。该线程负责周期性扫描可合并页面。
  3. 初始化全局链表与锁:初始化 ksm_mm_head 链表(用于管理所有 ksm_mm_slot)以及 ksm_thread_mutex 互斥锁。
  4. 注册 sysfs 接口:通过 kernel_subsys 创建 /sys/kernel/mm/ksm/ 目录,暴露 run、sleep_millisecs、pages_to_scan 等控制参数。
  5. 设置默认参数: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 资源。
KSM 模块初始化流程 ksm_init() 入口 (module_init) 初始化全局链表与锁 创建 ksmd 内核线程 注册 sysfs 接口 设置默认参数 (ksm_run=0)

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。