2. KSM 内核架构:模块位置、关键数据结构与扫描线程模型

KSM(Kernel Same-page Merging)是 Linux 内核中一项基于内存去重(Deduplication)的虚拟化优化技术。理解 KSM 在内核中的模块位置、核心数据结构以及扫描线程模型,是进行后续合并配置与效率调优的理论基础。本章将从内核源码视角,拆解 KSM 的静态结构与动态行为。

2.1 KSM 在内核中的模块位置

KSM 并非独立的内核模块,而是深度嵌入内存管理子系统(mm/)中的核心功能。其源码路径与依赖关系如下表所示:

源码路径 功能描述 关键依赖
mm/ksm.c KSM 主逻辑:扫描、比较、合并、回退 mm/memory.c、mm/rmap.c
include/linux/ksm.h KSM 对外接口与数据结构声明 mm_types.h、rmap.h
mm/memory.c 提供页表操作、COW(写时复制)触发回调 KSM 通过 do_wp_page() 触发拆分
mm/rmap.c 反向映射(Reverse Mapping)支持 KSM 合并后需要更新所有映射
架构要点: KSM 位于 mm/ksm.c,约 4000+ 行代码。它不直接操作硬件页表,而是通过标准内存管理接口(get_user_pages()、page_add_rmap())与页表交互。合并后的页面会被标记为 PageKsm 标志位,以区别于普通匿名页。
KSM 在内核内存子系统中的模块位置 用户态进程 / QEMU / 虚拟机 KSM 核心 (mm/ksm.c) 扫描 → 比较 → 合并 → 回退 mm/memory.c 页表操作 / COW mm/rmap.c 反向映射 mm/page_alloc.c 页面分配 / 回收 硬件页表 / TLB / MMU

2.2 关键数据结构

KSM 使用三棵红黑树(Red-Black Tree)和一个链表来管理扫描状态与合并结果。理解这些数据结构是掌握 KSM 内存布局的关键。

2.2.1 ksm_mm_slot —— 进程扫描上下文

每个注册了 KSM 的进程(通过 madvise(MADV_MERGEABLE))对应一个 ksm_mm_slot 实例。它记录了该进程的扫描进度和候选页面链表。

// 位于 include/linux/ksm.h
struct ksm_mm_slot {
    struct mm_struct *mm;           // 指向进程的内存描述符
    struct ksm_scan scan;           // 当前扫描位置(地址、索引)
    struct list_head mm_list;       // 链接到全局 ksm_mm_head 链表
    struct list_head rmap_list;     // 该进程下所有可合并页面的链表
};
调优关联: ksm_mm_slot 中的 scan 字段记录了上次扫描中断的虚拟地址。当 pages_to_scan 参数较小时,KSM 会在多个进程间轮转扫描,避免单进程饥饿。

2.2.2 stable_tree —— 稳定合并树

stable_tree 是一棵全局红黑树,以合并后页面的物理页帧号(PFN)为键值。所有已经被 KSM 合并的页面(即 PageKsm 页面)都挂在这棵树上。当扫描到新页面时,KSM 首先在 stable_tree 中查找内容完全相同的页面。

  • 键值: 合并页面的 page->index(实际存储的是 PFN 的哈希值)。
  • 节点: struct stable_node,包含指向合并页面的指针、引用计数、以及反向映射链表。
  • 作用: 加速查找,避免对已合并页面重复比较。

2.2.3 unstable_tree —— 不稳定候选树

unstable_tree 也是一棵全局红黑树,但存储的是尚未被合并、但内容可能重复的候选页面。KSM 在扫描过程中,将新页面与 unstable_tree 中的页面进行内容比较(使用 memcmp)。

  • 生命周期: 节点在每次扫描周期结束时被清空,因为页面内容可能已变化。
  • 节点: struct unstable_node,包含页面指针和校验和(Checksum)。
  • 性能影响: unstable_tree 的大小直接影响每次扫描的 CPU 开销。

2.2.4 数据结构关系总览

KSM 核心数据结构关系图 ksm_mm_head (全局链表) ksm_mm_slot mm_struct *mm ksm_mm_slot mm_struct *mm ... stable_tree (红黑树) 已合并页面 (PageKsm) unstable_tree (红黑树) 候选页面 (未合并) stable_node stable_node unstable_node 数据结构对比 stable_tree: 持久存在 unstable_tree: 每周期清空 ksm_mm_slot: 进程粒度

2.3 扫描线程模型

KSM 的扫描工作由内核线程 ksmd 驱动。该线程在 mm/ksm.c 中通过 kthread_run() 创建,优先级为 SCHED_NORMAL,但可通过 ksm_scan_thread 函数调整其调度策略。

2.3.1 扫描流程(逐周期)

  1. 遍历 ksm_mm_head 链表: 获取下一个待扫描的 ksm_mm_slot。
  2. 获取候选页面: 从进程的 rmap_list 中取出一个页面,通过 get_user_pages() 锁定。
  3. 计算校验和: 对页面内容计算 32 位 CRC 校验和,用于快速过滤。
  4. 查找 stable_tree: 如果校验和匹配,则尝试在稳定树中查找完全相同的页面。
  5. 查找 unstable_tree: 如果稳定树未命中,则在不稳定树中查找;若找到,则执行合并。
  6. 更新树结构: 合并成功后,将页面移入 stable_tree;否则插入 unstable_tree。
  7. 睡眠等待: 完成 pages_to_scan 个页面的扫描后,线程进入可中断睡眠,等待下次唤醒。

2.3.2 扫描参数与线程行为

参数(/sys/kernel/mm/ksm/) 默认值 对线程模型的影响
sleep_millisecs 20 每次扫描周期后的睡眠时间(毫秒),控制 CPU 占用率
pages_to_scan 100 每次唤醒后扫描的页面数,决定单次扫描的批处理大小
run 1 设置为 0 停止扫描,1 启动扫描,2 取消合并但不扫描
merge_across_nodes 1 是否允许跨 NUMA 节点合并,影响扫描线程的节点亲和性
性能陷阱: 当 sleep_millisecs 设置过小(如 < 10)且 pages_to_scan 过大(如 > 1000)时,ksmd 线程可能占据单核 100% CPU,导致业务进程抖动。建议在生产环境中通过 perf top 监控 ksmd 的 CPU 占用率。

2.3.3 线程状态转换

ksmd 线程状态转换图 SLEEPING 可中断睡眠 SCANNING 遍历页面 & 比较 MERGING 执行合并/回退 STOPPED run=0 或模块卸载 定时器到期 找到匹配 完成 pages_to_scan run=0

2.4 扫描效率的关键约束

从架构角度看,KSM 的扫描效率受以下三个内核级因素制约:

  • 红黑树查找开销: stable_tree 和 unstable_tree 的查找时间复杂度为 O(log N),其中 N 为树中节点数。当合并页面数超过 10 万时,查找延迟会显著增加。
  • 页面锁定与校验和计算: 每次扫描都需要通过 get_user_pages() 锁定页面,并对 4KB 内容计算 CRC。这是 CPU 密集操作。
  • 反向映射更新: 合并或回退时,需要遍历所有映射了该页面的 VMA,更新页表项。VMA 数量越多,延迟越高。
工程启示: 在后续章节中,我们将通过调整 pages_to_scan、sleep_millisecs 以及启用 merge_across_nodes=0 来缓解上述瓶颈。对于超大内存场景(>512GB),建议配合 numa_balancing 和透明大页(THP)一起调优。