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 标志位,以区别于普通匿名页。
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 数据结构关系总览
2.3 扫描线程模型
KSM 的扫描工作由内核线程 ksmd 驱动。该线程在 mm/ksm.c 中通过 kthread_run() 创建,优先级为 SCHED_NORMAL,但可通过 ksm_scan_thread 函数调整其调度策略。
2.3.1 扫描流程(逐周期)
- 遍历
ksm_mm_head链表: 获取下一个待扫描的ksm_mm_slot。 - 获取候选页面: 从进程的
rmap_list中取出一个页面,通过get_user_pages()锁定。 - 计算校验和: 对页面内容计算 32 位 CRC 校验和,用于快速过滤。
- 查找
stable_tree: 如果校验和匹配,则尝试在稳定树中查找完全相同的页面。 - 查找
unstable_tree: 如果稳定树未命中,则在不稳定树中查找;若找到,则执行合并。 - 更新树结构: 合并成功后,将页面移入
stable_tree;否则插入unstable_tree。 - 睡眠等待: 完成
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 线程状态转换
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)一起调优。