Skip to content

Latest commit

 

History

History
2541 lines (2043 loc) · 95.8 KB

File metadata and controls

2541 lines (2043 loc) · 95.8 KB

Linux 内核 KSM(Kernel Samepage Merging)深度分析

源码版本:基于 Linux mainline(分析时间:2026-03) 主要文件:mm/ksm.cinclude/linux/ksm.hmm/rmap.cmm/memory.c


目录

  1. KSM 设计背景与动机
  2. 核心数据结构
  3. 双红黑树设计:稳定树与不稳定树
  4. KSM 扫描流程
  5. 页面合并机制
  6. COW 触发与分裂
  7. EWMA 自动调速机制
  8. KSM 与 NUMA
  9. 侧信道攻击防护
  10. KSM 统计指标详解
  11. 进程级 KSM:MADV_MERGEABLE 与 prctl
  12. KSM 与 THP 的交互
  13. 反向映射(rmap)集成
  14. 内存迁移与热插拔支持
  15. sysfs 接口完整参考
  16. 性能分析与调优建议
  17. 关键代码路径总结

1. KSM 设计背景与动机

1.1 虚拟化场景的内存重复问题

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 进程)发现并合并相同页面。

1.2 与 zswap/zram 的互补关系

KSM、zswap 和 zram 是三种不同维度的内存压缩/节省技术:

内存优化技术对比:

  技术       作用层面        原理              副作用
  ──────────────────────────────────────────────────────────
  KSM        物理页去重      相同内容页共享     COW 延迟,扫描 CPU
  zswap      压缩换出        压缩 swap 页       CPU 压缩/解压开销
  zram       压缩 RAM 设备   内存中模拟块设备   同上
  HugePages  减少 TLB 压力   2MB/1GB 大页       碎片,不能 KSM 直接合并

  互补关系:
  KSM 消除重复  →  减少总物理用量
  zswap 压缩    →  延迟换出,提高 swap 效率
  二者可同时启用,针对不同内存压力场景

KSM 优先处理热内存中的重复页(写保护后共享),而 zswap 处理被换出的冷页(压缩存储)。在内存极度紧张时,KSM 的收益可能降低(很多页已被换出),此时 zswap 更有效。

1.3 性能与内存节省的权衡

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 元数据的内存开销超过了合并收益,需要减少候选区域。

1.4 整体架构图

用户空间                              内核空间
                              ┌──────────────────────────────────────────┐
  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 指向同一物理页)

2. 核心数据结构

2.1 ksm_rmap_item — 候选页的反向映射项

定义于 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)
  └──────────────────┘

2.2 ksm_stable_node — 稳定树节点

定义于 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 节点的垃圾回收频率。

2.3 ksm_mm_slot — 每个参与 KSM 的 mm 的槽

/* 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 扫描游标)

2.4 ksm_scan — 扫描游标

/* 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)递增一次,用于识别不稳定树节点是否属于当前轮次。

2.5 全局统计变量与常量

/* 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;

2.6 slab 缓存

/* 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;
    ...
}

3. 双红黑树设计:稳定树与不稳定树

3.1 树的存储结构

/* 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.

3.2 为何选择红黑树

代码注释(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) 依然有保证。同时搜索和插入算法相同,每轮清空重建无额外开销。

3.3 稳定树(Stable Tree)详解

特性

  • 存储所有已合并的 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);
}

3.4 不稳定树(Unstable Tree)详解

特性

  • 存储"连续两次扫描内容未变化"的候选页(通过 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;
}

搜索和插入合并为一次树遍历,这正是红黑树的优势所在。

3.5 稳定树与不稳定树的完整对比

特性               稳定树                      不稳定树
─────────────────────────────────────────────────────────────────────
节点类型           ksm_stable_node             ksm_rmap_item(双用)
节点持有页引用     不持有(keyhole 机制)       不持有
生命周期           持久(直到页被释放)          每轮全部清空重建
页状态             写保护只读(KSM 页)          普通匿名页(可变)
比较键             页内容(memcmp_pages)        同左
树可靠性           完全可靠                     不可靠(内容随时可变)
查找优先级         先查(更快,稳定)             稳定树未命中后才查
NUMA 支持          merge_across_nodes=0时分树    同左
最大节点数         无限(受物理内存限制)         受限(每轮清空)
chain/dup          支持(超过 max_page_sharing) 不涉及
搜索方式           只搜索(与插入分离)           搜索+插入合一(高效)

4. KSM 扫描流程

4.1 ksmd 内核线程的创建

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 在子系统初始化阶段启动,早于用户空间进程但晚于基本内存管理。

4.2 ksm_scan_thread() 主循环(mm/ksm.c:2801

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)。

4.3 ksm_do_scan() 批次扫描(mm/ksm.c:2780

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_npagesksm_thread_pages_to_scan,默认 100(DEFAULT_PAGES_TO_SCANmm/ksm.c:252)。每扫描一页都调用 cond_resched() 检查是否需要调度,保证 ksmd 不占用 CPU 过长。

4.4 scan_get_next_rmap_item() — 扫描引擎(mm/ksm.c:2574

此函数是 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.addressksm_scan.mm_slot 在每次 ksm_do_scan() 返回后保留,下次调用时从断点继续。这使得每批 100 个页面的扫描是精确连续的,不会漏掉或重复扫描。

4.5 页面哈希计算:full hash vs checksum

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%

5. 页面合并机制

5.1 cmp_and_merge_page() 总决策流程(mm/ksm.c:2248

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 已插入不稳定树,等待下次匹配

5.2 write_protect_page() 写保护流程(mm/ksm.c:1272

合并的第一步是将候选页设置为写保护。完整流程:

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 请求都无法再获得该页,从而消除了检查-写入之间的竞争窗口。

5.3 replace_page() PTE 替换(mm/ksm.c:1372

将候选页的 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,内核自动释放物理页,实现内存节省。

5.4 try_to_merge_one_page() — 单页合并(mm/ksm.c:1475

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() 成功后再关联。

5.5 try_to_merge_two_pages() — 两页首次合并(mm/ksm.c:1618

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 为普通匿名页。

5.6 完整合并调用链

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, ...)

6. COW 触发与分裂

6.1 KSM 页的 COW 机制

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 扫描时被发现并清理)

6.2 stable_node_dup 处理多版本

当 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;
}

6.3 ksm_might_need_to_copy() — swap-in 时的分叉处理(mm/ksm.c:3104

当曾经是 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 下次扫描时重新合并。

6.4 break_ksm() 强制解除合并(mm/ksm.c:692

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_UNMERGEABLEKSM_RUN_UNMERGE 均通过此路径解除合并。


7. EWMA 自动调速机制

7.1 advisor_ctx 上下文

/* 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,  /* 自动控制:按目标扫描时间调整 */
};

7.2 EWMA 算法(mm/ksm.c:376-378

/* 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 的变化较为平滑,避免因单次扫描时间波动导致的剧烈抖动。

7.3 scan_time_advisor() 详解(mm/ksm.c:402

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

7.4 advisor_mode 接口参数

/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_cpu

7.5 智能扫描(Smart Scan)

skip_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 开销。


8. KSM 与 NUMA

8.1 跨 NUMA 合并控制

/* 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
#endif

ksm_merge_across_nodes=1(默认):所有 NUMA 节点共享一棵稳定树和一棵不稳定树。

ksm_merge_across_nodes=0:每个 NUMA 节点有独立的稳定树和不稳定树数组。

8.2 跨节点合并的代价

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、实时系统

8.3 NUMA 迁移节点处理

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 节点的稳定树中,或清理失效节点。

8.4 per-NUMA stable tree 示意

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](每轮清空)
    ...

9. 侧信道攻击防护

9.1 Flush+Reload 攻击原理

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

9.2 Rowhammer 攻击与 KSM

Rowhammer 攻击通过高频访问相邻内存行诱发位翻转。KSM 与 Rowhammer 有间接关联:

传统 Rowhammer:
  攻击者占用物理内存 Row A 和 Row C
  高频访问 → 诱发 Row B(目标)位翻转

KSM + Rowhammer:
  KSM 合并后,攻击者和受害者共享同一物理页
  攻击者可以 COW 出一份写保护页的副本
  通过 Rowhammer 影响共享的物理行(受害者数据所在行)
  → 在某些内存拓扑下,扩大了攻击面

9.3 prctl(PR_SET_MEMORY_MERGE) 进程级控制

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_ANYMMF_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)

9.4 安全建议与缓解措施

内核层面的现有缓解

  1. ksm_max_page_sharing(默认 256):限制单个 KSM 页的最大共享数,减少攻击面
  2. use_zero_pages=0(默认):零页通过普通 KSM 流程合并而非直接映射已知的零页,减少可预测合并目标 3 smart_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 需要评估侧信道风险,尤其是当不同安全级别的工作负载在同一主机上运行时。


10. KSM 统计指标详解

10.1 核心统计指标

所有统计指标通过 /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     总扫描页数(累计)

10.2 pages_volatile 的计算(mm/ksm.c:3703-3717

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 都不同的页)。

10.3 实际内存节省计算

general_profitmm/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_sharedpages_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)。

10.4 统计指标间的关系

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(开销超过节省,需调整)

11. 进程级 KSM:MADV_MERGEABLE 与 prctl

11.1 madvise(MADV_MERGEABLE) 机制

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-756ksm_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;
}

11.2 __ksm_enter() 注册流程(mm/ksm.c:3015

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 的浪费。

11.3 KSM 对 mmap() 的影响

当进程设置了 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()

11.4 容器场景的 KSM 应用

在 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)启动容器时零页合并的基础。


12. KSM 与 THP 的交互

12.1 THP 不能直接 KSM 合并

透明大页(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 放弃本次合并机会,等待下一轮扫描时重试。

12.2 THP 与 KSM 的完整交互图

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_HUGEPAGEMADV_MERGEABLE

12.3 ksm_might_need_to_copy() 对大页的处理(mm/ksm.c:3111-3112

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 拆分后才能重新评估。

12.4 khugepaged 与 ksmd 的交互

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,避免两个守护线程相互干扰。


13. 反向映射(rmap)集成

13.1 KSM 页的特殊 rmap 需求

普通匿名页通过 anon_vma 链进行反向映射(从物理页找到所有映射它的 VMA 和 PTE)。KSM 页的映射横跨多个不相关进程(无父子关系),因此需要专门的 rmap 实现。

13.2 rmap_walk_ksm() — KSM 页的反向映射遍历(mm/ksm.c:3152

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)。

13.3 stable_tree_append() rmap 注册(mm/ksm.c:2205

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++;  /* 进程级计数 */
}

13.4 folio_migrate_ksm() — 页迁移时更新 kpfn(mm/ksm.c:3267

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 值,区分"页已迁移(应重试)"和"页已释放(应清理)"两种情况。


14. 内存迁移与热插拔支持

14.1 ksm_get_folio() 的 keyhole 引用机制(mm/ksm.c:946

这是一个精妙的无锁设计,使稳定节点不需要持有页引用计数:

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 字段存储反向指针(而非增加引用计数),避免了这一问题。

14.2 内存热插拔(CONFIG_MEMORY_HOTREMOVE

/* 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);
    }
}

14.3 内存下线时的稳定树清理

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 范围判断并直接删除,无需处理引用计数归零等问题。


15. sysfs 接口完整参考

15.1 可写控制参数

所有接口位于 /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 目标扫描时间(秒)

15.2 只读统计参数

统计指标                含义
────────────────────────────────────────────────────────────────────────
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 节点数量

15.3 KSM 运行状态机

         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 页恢复为私有匿名页

15.4 典型调优配置

# 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"

16. 性能分析与调优建议

16.1 KSM 的内存开销模型

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 参与

16.2 扫描速率与 CPU 开销估算

默认配置(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%(主要是内存访问延迟)

16.3 最优与不适用场景

最优场景

场景                  典型节省率    说明
──────────────────────────────────────────────────────────────────
KVM 多 guest          50-80%       相同 OS 内核、libc 大量重复
Kubernetes 容器       30-60%       相同基础镜像的多个 Pod
Java 应用集群         20-40%       JVM 运行时类元数据重复
Redis 多实例          40-70%       相同的数据结构内存布局

不适用场景

  • 实时系统(PREEMPT_RT):COW 触发的缺页延迟不可预测
  • 安全隔离要求高的多租户:侧信道攻击风险
  • 内存访问模式高度随机:扫描开销 > 合并收益
  • 已使用 THP 的工作负载(频繁 split/merge 循环)

16.4 KSM 与 cgroup 集成

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 内存配额

16.5 监控脚本示例

#!/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

17. 关键代码路径总结

17.1 进程注册路径

用户调用 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

17.2 扫描与合并路径

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)

17.3 COW 解除路径

进程写入 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()

17.4 文件与行号完整索引

功能 文件 行号
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 分析生成