Linux内核分析之内存管理-05

23.1 大页原理 —— 减少 TLB 失配

大页的收益常被粗略表述为"TLB 命中率高",本节用三组算式把收益拆开:TLB 覆盖倍增(同样条目数覆盖 512 倍地址)、页表遍历减负(中间层级消失)、缺页次数均摊(一次供货覆盖 512 个 4KB 单元),并逐项指出对应的成本(分配原子性、分裂搬运、碎片放大、迁移负担)。理解收益边界才能回答"何时该用大页、何时不该"。


23.1.1 收益一:TLB 覆盖的算术

TLB 是按"条目"计费的稀缺资源——现代 x86_64 的 L2 STLB 典型 1536-4096 条,ARM64 的 unified TLB 数量级相同——但系统真正消耗的是条目覆盖的地址范围:

TLB 覆盖 = 可用条目数 × 页大小

 条目 1536 条:
   4KB 页:   覆盖 6MB     ← 数据库缓冲池/JVM 老年代远超此值
   2MB 页:   覆盖 3GB     ← 覆盖率提升 512 倍
   1GB 页:   覆盖 1.5TB   ← 覆盖全部典型服务器内存

 TLB miss 的直接代价 (19.2.4 节四级树):
   页表不在缓存: 4 次内存访问 (PTE/中间级各一次)
   页表在 LLC:   ~4 次 LLC 延迟 (40-80ns 级)
   指针追逐型负载 (解释器/链表遍历) 的 miss 频率被放大

 公开基准的典型区间:
   SPEC CPU / PostgreSQL / Memcached: TLB miss 降 50-90%
   整体吞吐提升 5-30%, 强相关于 "工作集/页大小" 的比值

x86_64 还有一层不常提及的结构性收益:大页有独立的并行查找通道。TLB 是 VIPT 风格的并行比较阵列,2MB 页项存放在 PDE 大页通道、4KB 项存放在 PTE 通道——混用时两者并行查找互不挤占;全 4KB 时单一通道的容量墙被直接突破。

23.1.2 收益二:页表树的剪枝

19.2.4 节已给出布局结论(PS 位让 PMD/PUD 级直接变叶项),这里补全空间账与时间账:

映射 2MB 区域的页表成本:

 4KB 页方案:  1 张 PTE 表页(4KB, 512 项) + 占 1 个 PMD 表项
 2MB 页方案:  1 个 PMD 表项 (PS=1, 直接编码物理帧)

 单区域净省: 4KB 元数据 + 每次遍历少 1 级访存
 放大到 1TB 已映射内存:
   全 4KB:  PTE 表合计 2GB  (1TB/2MB × 4KB) + PMD 表 16MB
   全 2MB:  PMD 表 16MB     ← 元数据 500 倍差距
   全 1GB:  PUD 直达, 元数据仅数 MB
 访存账 (页表驻留 LLC 的保守估计):
   4KB 路径: PGD+PUD+PMD+PTE = 4 次
   2MB 路径: PGD+PUD+PMD    = 3 次

23.2.5 节的 HVO(vmemmap 优化)是对这笔元数据账的进一步压榨——连 512 个 struct page 描述符的冗余都要削掉。

23.1.3 收益三:缺页次数的均摊

触碰 2MB 区域的缺页次数:

 4KB 页: 最多 512 次缺页 (逐页供货, 22.4 节矩阵)
 2MB 页: 1 次 (do_huge_pmd_anonymous_page, huge_memory.c:1461)
          一次 9 阶分配 + 一个 PMD 块项映射

 每次缺页的真实成本构成:
   异常进入/退出 (约 1-2μs, 含 CR2/栈切换/上下文恢复)
   + VMA 查找与权限判定 (per-VMA 锁, 22.2.4 节)
   + 物理分配与页表写 (18.3/19 章)
   THP 直供把 512 次缩成 1 次 →
   大数组初始化/大文件顺序触顶的"缺页风暴"开销 ÷512

实测口径:4KB 路径上一次软缺页约 0.5-1μs、硬缺页(含分配)1-3μs,512 页的初始化多出 0.25-1.5ms 的纯异常开销——对延迟敏感服务是一次明显的长尾尖刺。

23.1.4 成本面:四笔反向账

成本 1: 分配原子性
  2MB = 9 阶伙伴请求, 1GB = 18 阶 (18.3 节)
  长寿命系统高阶连续页稀缺 (碎片化) → 分配延迟/失败
  THP 的应对: khugepaged 后台搬移合并 (23.3.3 节)
  HugeTLB 的应对: 启动期整块预留 (23.2 节池模型)

成本 2: 粒度变粗的内部碎片
  2MB 块只用 100KB → 1.9MB 白占 (匿名 THP 的典型病)
  对策: split_huge_page 分裂回收 (23.3.4 节)
        khugepaged_max_ptes_none 控制"空页多不合"

成本 3: 迁移与规整负担
  512 页必须一起搬 → compaction/migration 单次成本 ×512
  18.5 节 movable 机制仍有效但粒度变粗
  NUMA balancing 对 THP 的迁移决策因此更保守

成本 4: 回收不友好 (HugeTLB 特有)
  池页不可换出 → 内存对回收器"隐身"
  预留过多 = 系统可用内存直接缩水 (23.2.2 节)

23.1.5 三种大页粒度与两条路径的分工

 粒度        硬件基础              内核机制                      章节
 2MB        PMD 块项 (PS 位)      THP (自动) + HugeTLB 双路径    23.2/23.3
            (19.2.4 节)
 1GB        PUD 块项              HugeTLB 1GB 档 (hstate);      23.2
                                THP 的 PUD 版本实验性可配
 架构大页    ARM64 cont hint       contpte 折叠 (19.3.4 节)      (对照)
            16×4KB 合并 TLB

两条路径的本质分野是确定性 vs 透明性:

  • HugeTLB:应用显式申请(mmap(MAP_HUGETLB)/shmget(SHM_HUGETLB)),内核从预建池供货。确定性(分配永不失败——预订合同语义)与隔离性(页不可换出、不可回收)是卖点,内存刚性占用是代价。数据库 SGA、DPDK 巨页池、虚拟机大内存后端是典型客户。
  • THP:内核对普通匿名映射自动合并/直供,应用无感知(或仅 madvise 提示)。透明与弹性是卖点,延迟抖动与内部碎片是风险。一般服务负载的默认选择。

ARM64 的 contiguous hint(19.3.4 节)是第三种思路:不改页大小、只让 16 个连续 4KB 项共享一个 TLB 项——碎片免疫(无需任何物理连续性)但收益封顶 16 倍,与 2MB 大页构成细/粗两端的互补。RISC-V 的 megapage(2MB/1GB 块项)与 x86 同构但无 THP 实现(本树仅 hugetlb 路径),因此 23.4 节 RISC-V KVM 客户机的大页完全依赖 hugetlb 后端——两条路径的有无直接改变了虚拟化场景的配置策略。

23.1.6 判断框架:三问决定用不用

 [1] 工作集/6MB 的比值大吗?
     (1536 条 × 4KB = 6MB; 比值 >1 才有 TLB 账可算)
 [2] 访问模式的空间局部性强吗?
     顺序/成块访问 → 大页收益足额; 随机稀疏访问 → 内部碎片风险
 [3] 确定性要求高吗?
     分配失败不可接受 → HugeTLB 池; 可以接受回退 → THP

 三问皆是 → HugeTLB; 一二问是、第三问否 → THP;
 一问否 → 维持 4KB (或 ARM64 cont hint)

小结

大页的收益是三笔可计算的账:TLB 覆盖 ×512(含专用查找通道)、页表元数据 ÷500、缺页次数 ÷512;对应的成本是分配原子性、内部碎片、迁移负担与(HugeTLB 特有的)回收不友好。内核用两条路径分摊成本:HugeTLB 以"启动期预留换确定性",THP 以"后台搬运换透明性",ARM64 cont hint 提供免碎片的中庸选项,RISC-V 则只享其一。大页不是免费午餐而是资源配置决策——工作集比值、局部性与确定性要求的三问框架给出选择依据。下一节进入 HugeTLB 的预留池模型。

23.2 HugeTLB —— 预分配大页

HugeTLB 的设计哲学与 21 章 SLUB 同源:把分配决策提前,运行期只做池内存取。每档大页(2MB、1GB、架构自定义)由一个 hstate 描述,页从伙伴系统/启动内存整块进货进池,应用经 MAP_HUGETLB/SHM_HUGETLB 从池中取页;池页不换出、可超卖(surplus)、可降级(demote)、可溶解(dissolve),元数据经 HVO 压缩。本节拆解这套池模型的每个环节。


23.2.1 hstate:每档大页一个池

// include/linux/hugetlb.h:666-693
struct hstate {
    struct mutex resize_lock;
    struct lock_class_key resize_key;
    int next_nid_to_alloc;      /* 轮转分配的节点游标 */
    int next_nid_to_free;
    unsigned int order;     /* 伙伴系统阶: 2MB=9, 1GB=18 */
    unsigned int demote_order;  /* 降级目标档的阶 */
    unsigned long mask;
    unsigned long max_huge_pages;       /* 池上限(sysctl 可调) */
    unsigned long nr_huge_pages;        /* 池内总页 */
    unsigned long free_huge_pages;      /* 空闲页 */
    unsigned long resv_huge_pages;      /* 已被"预订"的页 */
    unsigned long surplus_huge_pages;   /* 超卖页(池外借用) */
    unsigned long nr_overcommit_huge_pages; /* 允许的超卖上限 */
    struct list_head hugepage_activelist;   /* 使用中页链(泄漏排查) */
    struct list_head hugepage_freelists[MAX_NUMNODES];  /* 每节点空闲链 */
    unsigned int max_huge_pages_node[MAX_NUMNODES];
    unsigned int nr_huge_pages_node[MAX_NUMNODES];
    unsigned int free_huge_pages_node[MAX_NUMNODES];
    unsigned int surplus_huge_pages_node[MAX_NUMNODES];
    char name[HSTATE_NAME_LEN];
};

多档并存:2MB(HUGETLB_PAGE_ORDER)档总是存在——hugetlb_init()(mm/hugetlb.c:4130)调用 hugetlb_add_hstate(HUGETLB_PAGE_ORDER)(:4148)默认建档;1GB 档在硬件支持时由 hugepagesz=/default_hugepagesz= 启动参数激活。每个 hstate 是完全独立的仓库——2MB 池与 1GB 池之间没有共享,调参面(/proc/sys/vm/nr_hugepages、/proc/sys/vm/nr_overcommit_hugepages 与 /sys/kernel/mm/hugepages/hugepages-*/ 两套入口)也按档独立。

池的四级账本是理解行为的钥匙:

nr_huge_pages   池内总页   = free + 已借出 + resv(未兑现) 部分
free_huge_pages 空闲页     分配从这里扣
resv_huge_pages 已预订页   mmap 合同锁定的份额 (见下)
surplus_huge_pages 超卖页  超出"实池"现场借用的页 (见下)

预订(resv)合同语义:

 mmap(MAP_HUGETLB, len) 立即: resv_huge_pages += 需要页数
   → 合同成立: 之后任何时刻触碰, 池里必有页可给
 fork: 合同随 VMA 继承 (hugetlb_vm_op_open, :4696)
 退出/解除映射: 合同作废, resv 归还 (hugetlb_vm_op_close, :4736)
   物理页从未消耗 — "预订了但没用"零成本

 超卖(surplus)语义: 预订总量 > 实池容量时,
 按 nr_overcommit_huge_pages 上限现场向伙伴系统借页补池
 → 以"可能分配失败"换池容量的弹性

23.2.2 取页路径与回池

// mm/hugetlb.c:1916-1921(现场补池入口)
static struct folio *alloc_fresh_hugetlb_folio(struct hstate *h,
            gfp_t gfp_mask, int nid, nodemask_t *nmask, ...)
{
    ...
    folio = only_alloc_fresh_hugetlb_folio(h, gfp_mask, nid, nmask, NULL);
    ...
}

补池的供货顺序(only_alloc_fresh_hugetlb_folio,:1889 起):hugetlb_cma 优先(hugetlb_cma_alloc_frozen_folio,:1418——hugetlb_cma= 参数预留的 CMA 区,页天然处于 MIGRATE_CMA 可借形态,18.3.4 节)→ 伙伴系统对应阶(9/18 阶请求,可带 NUMA 节点偏好)。取页给应用时再区分"池内现货"与"超卖新货":

// mm/hugetlb.c:2128-2182(超卖分配的完整防御, 节选)
static struct folio *alloc_surplus_hugetlb_folio(struct hstate *h,
                gfp_t gfp_mask, int nid, nodemask_t *nmask)
{
    struct folio *folio = NULL;

    if (hstate_is_gigantic_no_runtime(h))
        return NULL;        /* 1GB 页不许运行时超卖 */

    spin_lock_irq(&hugetlb_lock);
    if (h->surplus_huge_pages >= h->nr_overcommit_huge_pages)
        goto out_unlock;    /* 超卖额度用尽 */
    spin_unlock_irq(&hugetlb_lock);

    folio = alloc_fresh_hugetlb_folio(h, gfp_mask, nid, nmask);
    if (!folio)
        return NULL;

    spin_lock_irq(&hugetlb_lock);
    /*
     * nr_huge_pages needs to be adjusted within the same lock cycle
     * as surplus_pages, otherwise it might confuse
     * persistent_huge_pages() momentarily.
     */
    account_new_hugetlb_folio(h, folio);

    /*
     * We could have raced with the pool size change.
     * Double check that and simply deallocate the new page
     * if we would end up overcommiting the surpluses...
     */
    if (h->surplus_huge_pages >= h->nr_overcommit_huge_pages) {
        folio_set_hugetlb_temporary(folio);
        spin_unlock_irq(&hugetlb_lock);
        free_huge_folio(folio);
        return NULL;        /* 双检失败: 刚进的货立即退回 */
    }

    h->surplus_huge_pages++;
    h->surplus_huge_pages_node[folio_nid(folio)]++;
    ...
}

这段代码是锁窗口纪律的范本:额度检查(第一次 spin 段)→ 放锁做昂贵分配 → 回锁记账("nr 与 surplus 必须同一锁周期调整"的注释)→ 二次复核(池大小可能在放锁期间被 sysctl 改小),失败页打 hugetlb_temporary 标记走专门释放流——注释自嘲"Abuse temporary page to workaround the nasty free_huge_folio codeflow",可见该路径的微妙。回池方向由 enqueue_hugetlb_folio()(:1287)承接:按页所在节点挂 hugepage_freelists[nid] 链并累加 free 计数;__update_and_free_hugetlb_folio()(:1511)处理"页彻底退出池"的复杂清理(解除 HVO、页类型复位、还伙伴系统)。hugepage_activelist(hugetlb.h:688)把在用页挂链,/proc/meminfo 的 HugePages 各行与泄漏排查都以这套账为底。

HugeTLB 页永不换出的原因不是某个标志位的禁止,而是语义:页在池中有独立身份(可能来自 CMA/独立池)、可能承载进程间共享的不可复制映射。回收器(24 章)对 folio_test_hugetlb 直接跳过——代价是这些页对常规内存管理完全不可见,预留过多即系统可用内存等量缩水(23.1.4 节成本 4)。

23.2.3 大页缺页路径:hugetlb_fault

HugeTLB 映射不进入普通缺页矩阵:其 VMA 挂专用操作表——

// mm/hugetlb.c:4830-4834
const struct vm_operations_struct hugetlb_vm_ops = {
    .fault = hugetlb_vm_op_fault,       /* :4818 */
    .open = hugetlb_vm_op_open,     /* :4696 合同继承 */
    .close = hugetlb_vm_op_close,       /* :4736 合同解除 */
    ...
};

异常入口按 VMA 类型分流到 hugetlb_fault()(:5975)。与 22.4 节普通路径的差异表:

环节 普通页 (22.4) HugeTLB
分配时机 首触缺页现场分配 缺页时从池取(预订合同已保证有货)
页表层级 PTE(或 THP 的 PMD) hstate 阶对应层(PMD/PUD/自定义档)
分配失败语义 回退零页/逐页重试 合同内必成功;超卖外才 -ENOMEM
换出/回收 LRU → swap/丢弃 无,仅可迁移(NUMA 移动)
记账 anon/file + memcg hstate 池账 + hugetlb cgroup(12.4 节)

hugetlb_vm_op_open/close 两个钩子是预订合同的执行点:fork 时 open 按共享/私有语义接转 resv 份额,munmap/退出时 close 归还未兑现部分。私有映射的 resv 计数还与 hugetlb_page_subpool(VMA 所属子池)联动,保证"同一子池的多个映射不会重复预订"。

23.2.4 demote 与 dissolve:池的两种出清

// hugetlb.h:672
    unsigned int demote_order;

demote(降级):把 1GB 页拆成 512 个 2MB 页补 2MB 池(/sys/kernel/mm/hugepages/hugepages-1048576kB/demote 写入触发,内核经 demote_order 循环执行)。池结构决定"1GB 预多了、2MB 不够"时不必回炉重造。反向操作不存在:把 512 个 2MB 合回 1GB 需要物理连续性重聚,是伙伴系统单向合并(18.3.3 节)都不保证的稀缺事件——高阶连续性是单向稀缺资源。

dissolve(溶解):运行时缩小池(set_max_huge_pages(),:3780)时,空闲页需退出大页身份、回到伙伴系统。dissolve_free_hugetlb_folio()(:2014)处理"页正被 reserved/超卖引用"的拒绝情形;dissolve_free_hugetlb_folios()(:2101)是热插拔下线路径(18.5 节 do_migrate_range 的 hugetlb 前哨)批量溶解接口——注释明确"返回错误时调用方不得继续 offline"。

23.2.5 HVO:vmemmap 元数据压缩

20.4.1 节已指出对齐巧合:2MB 大页对应 512 个 page 槽 = 恰好 8 页 vmemmap 内存,但只有头 page 承载真实元数据。HVO(Hugetlb Vmemmap Optimization)把这 8 页压成 1 页:

// mm/hugetlb_vmemmap.c:448-492(要点)
static int __init hugetlb_vmemmap_optimize_param(char *buf) /* :448 启动参数 */
...
    if (!folio_test_hugetlb_vmemmap_optimized(folio))   /* :464 */
        ...
        folio_clear_hugetlb_vmemmap_optimized(folio);   /* :483 解优化 */
HVO 前后 (1 个 2MB 大页):

 前: vmemmap 8 页 (512×64B) 承载 page 数组
 后: vmemmap 1 页 — 头 page 槽位真实;
     511 个尾槽全部映射到头页同一物理页 (只读, 内容无意义)
     → 每页净省 7×4KB = 28KB ≈ 1.4% 元数据开销
     1GB 档: 省 4096×4KB ≈ 14MB

 约束: 优化态大页分裂/迁移/溶解前必须先解优化
      (重建完整 vmemmap 页表) — hugetlb 页本不可分裂,
      故仅 迁移( NUMA 移动) 与 demote/dissolve 路径需要

hugetlb_free_vmemmap 启动参数(:452 的 early_param)与 /sys/kernel/mm/hugepages/.../free_vmemmap_pages 运行时开关控制该优化;1% 量级的内存回收对超大池(TB 级)是实打实的 GB 级节约。

23.2.6 配置与观测

启动参数:
  hugepagesz=1G hugepages=4          1GB 池预建 4 页
  default_hugepagesz=2M hugepages=256  默认档 + 池大小
  hugetlb_cma=16G                    CMA 预留 (规避启动后碎片化)
运行时:
  /proc/sys/vm/nr_hugepages{,_mempolicy}   池扩缩
  /proc/sys/vm/nr_overcommit_hugepages     超卖上限
  /sys/kernel/mm/hugepages/hugepages-*/    全套参数视图
观测:
  /proc/meminfo: HugePages_Total/Free/Reserved/Surplus/Hugepagesize
  /sys/kernel/mm/hugepages/.../nr_huge_pages_node*  每节点分布
  /proc/pid/smaps: AnonHugePages 之外的 KernelPageSize/MMUPageSize
   可辨识 hugetlb 映射

小结

HugeTLB 以 hstate 为单位管理"每档大页一个池":四级账本(nr/free/resv/surplus)支撑"预订合同 + 超卖弹性"的容量模型,hugetlb_vm_op_open/close 是合同的执行点,hugetlb_fault 提供绕开普通缺页矩阵的专属供货路径,alloc_surplus_hugetlb_folio 的双检协议示范了池账在并发下的锁窗口纪律。demote 提供大档向小档的单向调整、dissolve 支撑池收缩与热插拔、HVO 削掉约 1.4% 的元数据开销。池模型放弃换出与透明性,换来启动期可验证的确定性——这正是它服务数据库/DPDK 类"预算刚性"负载的资本。下一节看不要求预算刚性的另一条路:透明大页。

23.3 透明大页 (THP) —— 自动大页合并

THP 的承诺是"应用零改动获得大页":匿名映射的 2MB 对齐区域优先直供大页(缺页直供),未赶上直供的已存数据由 khugepaged 后台扫描合并(collapse),不再适合大页的区域随时分裂回 4KB(split)。三条子路径构成动态均衡,全部由准入检查 __thp_vma_allowable_orders() 把关。本节沿"准入 → 直供 → 收缩 → 分裂 → 代价治理"的闭环展开。


23.3.1 准入:thp_vma_allowable_orders

// mm/huge_memory.c:103(签名)
unsigned long __thp_vma_allowable_orders(struct vm_area_struct *vma,
            unsigned long vm_flags, unsigned long tva_flags,
            unsigned long orders)

准入是多条件合取,返回值是"允许的阶位掩码"——天然支持多档(PMD 2MB 与实验性 PUD 1GB)一次裁决:

准入检查清单:

 [1] VMA 类型: 匿名 / shmem / 可读文件映射 (逐类开关)
 [2] 对齐: 区间起点与长度 PMD 对齐 (文件映射可由
     thp_get_unmapped_area 提前经营, 见 23.3.2)
 [3] 非 VM_SPECIAL: VM_IO|VM_PFNMAP|VM_DONTEXPAND 等
     (22.3.2 节合并否决项的同族 — "长得像但语义不可并")
 [4] 系统策略: transparent_hugepage/enabled 的
     always / madvise / never 三态
 [5] 进程策略: prctl(PR_SET_THP_DISABLE, 1) 的进程级退出
 [6] memcg/容量前置: 膨胀预警路径 (once per VMA)

madvise 模式的流行源于 THP 的延迟副作用(23.3.5 节):数据库等延迟敏感负载用 madvise(MADV_HUGEPAGE) 精准开放区间、其余区域全局不试——把"自动"的粒度从进程级细化到区间级。khugepaged_enter_vma()(mm/khugepaged.c:454)在 VMA 建立时按准入结果把它登记进扫描集。

23.3.2 直供:do_huge_pmd_anonymous_page

// mm/huge_memory.c:1323 与 1461(要点)
static vm_fault_t __do_huge_pmd_anonymous_page(struct vm_fault *vmf)    /* :1323 */
{   ...
    /* 一次性分配 2MB movable 大页 (18.3 节 9 阶请求) */
    folio = vma_alloc_zeroed_movable_folio(vma, haddr, HPAGE_PMD_ORDER);
    if (!folio)
        goto fallback;
    ...
    /* 原子换装: PMD 项从"指向 PTE 表"变为"块项(PS=1)" */
    ...
}

vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)     /* :1461 */
{ ... /* 准入+对齐检查后转发, 任何失败静默回退 4KB 路径 */ }

直供发生在 22.4 节缺页矩阵的入口前段:VMA 准入且地址 2MB 对齐 → 一次 2MB 分配(HPAGE_PMD_ORDER 取自 huge_mm.h:117)→ 一个 PMD 块项完成 512 页供货。失败回退是静默的——高阶分配失败(18.3 节的常态)退回 do_anonymous_page 逐页供货,应用无感知。这就是"透明"的实现本质:每一步都有 4KB 退路。

文件映射的对齐要提前经营:thp_get_unmapped_area()(huge_memory.c:1249-1254 → __thp_get_unmapped_area :1189 以 PMD_SIZE 为粒度选址)挂在文件映射的 get_unmapped_area 路径上,让库/大文件的 mmap 起点天然落在 PMD 边界——没有这一步,缺页直供对文件映射几乎永不命中(起点随机)。匿名路径则靠 22.1 节布局中 mmap 区的天然对齐向下分配。

23.3.3 khugepaged:后台收缩

已存的 4KB 离散页(启动早期、直供失败后逐页触碰的)由 khugepaged 内核线程定期扫描合并:

// mm/khugepaged.c:1078 与 531(收缩主体与准入细查)
static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long address,
                       ...)     /* :1078 */
{   /* mmap_lock 写锁升级/降级舞蹈: 先读锁扫描,
       竞争激烈时退出让路, 拿到写锁才动页表 */ }

static enum scan_result __collapse_huge_page_isolate(struct vm_area_struct *vma,
            unsigned long address, pte_t *pte, ...) /* :531 */
{   /*
     * 逐 pte 体检 512 个 4KB 页能否整体打包:
     *   pte 必须存在/同类/可迁移/未被 GUP pin
     *   swap 槽页可同步换回 (__collapse_huge_page_swapin, :974)
     *   有一个不合格 → 放弃本区, SCANS 计数
     */
    trace_mm_collapse_huge_page_isolate(folio, none_or_zero, ...);  /* :659 */
}

static void __collapse_huge_page_copy_succeeded/failed(...) /* :670/:734 */
{ ... /* 逐页拷贝到新 2MB 页; 失败路径回滚已拷部分 */ }
khugepaged 的一次扫描循环:

 khugepaged_do_scan()                       khugepaged.c:2544
   └─ khugepaged_scan_mm_slot(pages, ...)   :2388
        按页数预算遍历登记的 mm
        ├─ 候选地址: __collapse_huge_page_isolate (:531)
        │    512 项逐个体检 → 全合格才继续
        ├─ collapse_huge_page (:1078)
        │    分配新 2MB 页 → 拷贝 → 原子换 PMD
        │    → 旧页逐个释放 (进入伙伴系统可合并)
        └─ pages 预算耗尽即让路 (调度友好)
        scan_sleep_millisecs / alloc_sleep_millisecs
        控制扫描节拍与分配失败的退避

收缩的写侧成本(拷贝 2MB + 页表手术)被页数预算摊薄;swap 槽页的现场换回(:974 __collapse_huge_page_swapin)让"半换出的冷区"也有机会合并。MADV_COLLAPSE(madvise_collapse,khugepaged.c:2756)允许应用同步要求收缩并拿到成败与错误码——注释(:2725-2728)明言它"打破 madvise(2) 惯例以提供同步语义"(madvise_collapse_errno 把扫描结果翻译成 errno)。延迟敏感场景在预热阶段主动收缩,把 khugepaged 的异步不确定性换成一次性可测成本。

23.3.4 split:分裂与延迟分裂队列

反向操作在两类时机触发:粒度需要(mprotect/madvise 修改 2MB 区间的一部分、部分 unmap、KSM 合并前)、内存治理(页 512 个里只有少数在用)。分裂不是简单的"重建 PTE 表"——它被刻意延迟并与回收合流:

// mm/huge_memory.c:73-80 与 1083-1102(延迟分裂队列)
static unsigned long deferred_split_scan(struct shrinker *shrink,   /* :73 */
                     struct shrink_control *sc)
{ ... /* 从每节点/每 memcg 队列批量取出待分裂 THP 执行 */ }
    ...
static struct deferred_split *split_queue_node(int nid)     /* :1083 */
{ return &pgdat->deferred_split_queue; }    /* 18.2.4 节 pgdat 字段 */

分裂请求先进 deferred_split 队列(挂在 pgdat 或 memcg 的队列结构上——18.2.3 节 pglist_data 的 deferred_split_queue 字段),由注册为 shrinker 的 deferred_split_scan(huge_memory.c:870 注册)在内存压力时批量执行。设计动机:分裂 2MB 页本质是"制造 512 个单页回收候选",先分裂、让回收器顺手带走零引用页比立即分裂再单独回收省一遍全表遍历——分裂与回收在 shrinker 框架里天然合流。

23.3.5 代价的账面与调参

THP 的三笔负账与内核对策:

 1. 内存膨胀 (内部碎片):
    100KB 数据占 2MB → 空页占比过高时 khugepaged 拒绝合并
    khugepaged_max_ptes_none: 一个 2MB 区间内"空 pte"超过
    该值不合并 (默认偏保守, 数值可调 — 语义是"至少用了
    多少页才值得合并", 23.1.4 成本 2 的执行点)
    系统内存吃紧时: THP 分裂优先级升高 (shrinker 联动)

 2. 分配延迟尖刺:
    缺页直供的 2MB 分配可能同步触发回收/压缩 (慢数 ms)
    对策: defrag=always/madvise/defer/defer+madvise
    defer 档把"重活"移交 khugepaged, 缺页路径只做轻试

 3. 分裂/搬运的 CPU:
    collapse 拷 2MB + 512 项体检, split 重建 PTE 表
    对策: 页数预算 + 延迟分裂 + madvise 圈地 + MCOLLAPSE 显式化

调参面: /sys/kernel/mm/transparent_hugepage/
  enabled / defrag / hpage_pmd_size
  khugepaged/{scan_sleep_millisecs, pages_to_scan,
              max_ptes_none, max_ptes_swap, ...}
观测面: /proc/pid/smaps 的 AnonHugePages;
  /proc/vmstat 的 thp_fault_alloc / thp_collapse /
  thp_split_page / thp_deferred_split_page

"启用 THP 后某服务 p99 延迟上升"的标准排查序:先看 thp_split_page 是否与延迟尖刺同刻(分裂风暴)→ 检查负载是否小对象密集(内部碎片)→ 评估从 always 降档 madvise 或用 PR_SET_THP_DISABLE——三条对策分别对应三笔负账。


小结

THP 用"准入 → 直供 → 收缩 → 分裂"四段闭环实现透明化:__thp_vma_allowable_orders 统一裁决(madvise 模式提供区间级旋钮),do_huge_pmd_anonymous_page 带静默回退地直供(文件映射靠 thp_get_unmapped_area 提前经营对齐),khugepaged 以页数预算与 512 项体检把离散页慢慢收编(MADV_COLLAPSE 提供同步通道),deferred split 队列把分裂与回收在 shrinker 框架里合流。透明性的代价——膨胀、尖刺、CPU——分别由 max_ptes_none、defrag 档位与页数预算显式管理。下一节看虚拟化场景如何把大页收益放大一倍。

23.4 大页与 KVM 虚拟化

虚拟化为地址翻译叠加了一层:客户机虚拟地址(GVA)→ 客户机物理地址(GPA)→ 宿主机物理地址(HPA)的两段翻译(49 章展开 KVM 架构)。硬件辅助的两级页表(x86 EPT/NPT、ARM64 Stage-2、RISC-V G-stage)让成本从"软件影子页表的两遍遍历"降为"硬件自动二级遍历",但缺一级大页,成本就乘一次 512——大页在虚拟化里是双倍杠杆。本节分析两级大页如何建立、宿主/客户机的配置协同,以及大页的退化路径与排查。


23.4.1 二级翻译的倍增结构

非虚拟化 (19 章单级翻译):
  GVA ──[4 级页表]──> HPA          TLB 缓存整条链

虚拟化 + 硬件两级:
  GVA ──[客户机 4 级]──> GPA ──[EPT/S2/G-stage 4 级]──> HPA
  两段各自最多 4 级 → 最坏 9 次内存访问/TLB miss
  TLB 需同时缓存 GVA→HPA 组合项 与 GPA→HPA 项

 大页对两级的意义:
  GVA 段 2MB 大页: 客户机页表少 1 级 (19.2.4 节)
  GPA 段 2MB 大页: EPT 少 1 级
  两段都 2MB:      最坏访存 9→2 次, TLB 条目覆盖 ×512×512
                   (组合项覆盖 = 客户机页 × 物理映射粒度)

两级各有独立的缺页语义:客户机页表由客户机内核自管(对宿主透明),EPT/S2 由宿主 KVM 维护——GPA→HPA 映射在宿主内存被迁移/换出/规整时需要 EPT 项的失效与重建(宿主缺页的镜像)。大页减少的不只是遍历级数,还有第二级的 EPT 缺页次数:512 个 4KB GPA 页映射宿主连续 2MB 时,EPT 一次建成;宿主 THP 分裂则 EPT 缺页 ×512。

ARM64 的两级命名(5.2/19.3 节)对应:TTBR0/1 承载客户机 Stage-1,VTTBR_EL2 承载 Stage-2;RISC-V 的 HGATP(6.8 节 H 扩展)同理。

23.4.2 KVM 侧:EPT 大页的建立与粒度语言

// arch/x86/include/asm/kvm_host.h:157-161
#define KVM_HPAGE_GFN_SHIFT(x)  (((x) - 1) * 9)
#define KVM_HPAGE_SHIFT(x)  (PAGE_SHIFT + KVM_HPAGE_GFN_SHIFT(x))
#define KVM_HPAGE_SIZE(x)   (1UL << KVM_HPAGE_SHIFT(x))
#define KVM_HPAGE_MASK(x)   (~(KVM_HPAGE_SIZE(x) - 1))
#define KVM_PAGES_PER_HPAGE(x)  (KVM_HPAGE_SIZE(x) / PAGE_SIZE)

x 是 KVM 页表层级(1=PTE 4KB、2=PMD 2MB、3=PUD 1GB),这组宏是 KVM 内存虚拟化代码的统一粒度语言——所有"这个 spte 覆盖多大"的判断都经它表达。EPT 项的生命周期:

建立 (fault 驱动, 逐级):
  客户机首次访问 GPA → KVM EPT 缺页 → 填最小粒度 4KB 项
  → fast_page_fault/对齐窗口检测: 连续 GPA 映射连续 HPA
    且宿主 VMA 大页就绪 (THP 2MB/1GB 或 hugetlb 后端)
  → promote: 合并 512 个 4KB 项为 1 个 2MB 项 (原子换装)

失效与重建:
  宿主内存迁移/规整 → EPT 项失效 → 客户机下次访问再缺页
  KSM 合并/NUMA balancing → 同上 (平衡收益与 EPT 抖动)

降级 (demote/split):
  NX 追踪: 影子执行权限冲突的页 (nx_huge_page 机制,
  kvm_host.h:1378-1380 注释 — nx_huge_page_disallowed)
  临时以"NX 大页"覆盖或拆成 4KB 以精细控执行权限
  宿主 THP 分裂 → EPT 2MB 项失去宿主连续性 → 被动拆分

宿主侧的大页来源正是本章前两节的成果:宿主 THP(KVM 建立 EPT 大页前查询宿主 VMA 的大页状态——anon THP 2MB 就绪则 EPT 可用 2MB)与宿主 HugeTLB 后备内存(QEMU -object memory-backend-memfd,hugetlb=on 或 hugetlbfs 文件后端——整客户机内存都在 2MB/1GB 大页上,EPT 天然大页且无 THP 延迟抖动,23.2 节池模型的确定性在此兑现)。

23.4.3 客户机侧:让 GVA 段也用大页

两段杠杆要同时扳动,缺一段收益减半:

客户机配置清单 (典型 DB/大内存负载):

 [1] 客户机内核开 THP (或显式 HugeTLB)
     → GVA 段 2MB 大页: 客户机 TLB/页表同享 23.1 节收益
 [2] 宿主机 THP=always/madvise 或 hugetlbfs 后端
     → EPT 段 2MB/1GB 大页
 [3] 关闭 EPT 大页劣化源:
     宿主内存规整对 KVM 内存的迁移扰动 (numa_balancing 调优)
     NX huge page split 的触发面 (频繁 W^X 翻转的负载)
 [4] 内存后端选择:
     memfd+THP (弹性, 23.3 节) vs hugetlbfs (确定性, 23.2 节)
 [5] 大页对齐的内存布局 (客户机内):
     客户机内核的 THP 对齐选址 (23.3.2 节) 自动生效

23.4.4 退化的连锁反应与排查

退化链: 宿主 THP 分裂 (23.3.4 节)
   → EPT 2MB 项失去宿主连续性 → 拆成 512×4KB
   → EPT 条目数 ×512 → TLB 池被 EPT 项挤占
   → 客户机 TLB 命中率下滑 → 客户机内性能回归
   (宿主看 CPU 轻载, 客户机看慢 — 经典的错位观测)

排查序:
 1. 宿主 /proc/vmstat: thp_split_page 与回归时间窗对齐
 2. KVM 统计: /sys/kernel/debug/kvm/*/ 大页 spte 计数
   (mmu_page_* 分布 — 2M/1G 级 spte 数量是否塌方)
 3. 客户机内: /proc/pid/smaps 的 AnonHugePages
   (GVA 段是否也退化了 — 两段分别诊断)
 4. 定位劣化源: 内存规整? NUMA balancing? NX 追踪?
   逐项临时关闭做差分

23.4.5 三架构的二级大页对照

维度 x86 (EPT/NPT) ARM64 (S2) RISC-V (G-stage)
二级大页粒度 2MB/1GB(PDE/PDPTE 块) 2MB/1GB(Block 描述符,19.3.3 节) 2MB/1GB(megapage)
宿主大页来源 THP/HugeTLB 双轨 THP/HugeTLB(5.7 节 MTE 需注意) 仅 HugeTLB(无 THP,23.1.5 节)
大页提升时机 fault 后 promote(对齐窗口) S2 建表时查询宿主直接块级建立 同左
劣化源 NX 追踪拆分 S2 权限变更/Stage-1 变更 较少(实现较新)
确定性后端 hugetlbfs/memfd 同左 唯一选择

RISC-V 宿主机因缺 THP,其 KVM 客户机的 G-stage 大页几乎完全依赖 HugeTLB 后备——23.2 节池模型在 RISC-V 虚拟化中的地位比 x86 更核心;这也意味着 RISC-V 宿主需要在启动参数里为虚拟机内存做整体大页预算(hugepagesz=1G hugepages=N),弹性缺失由容量规划补足。


小结

虚拟化把地址翻译变成两级,每缺一级大页就把最坏访存从 4 级翻倍到 8-9 级,且第二级(EPT/S2/G-stage)的缺页与失效成本同样被粒度放大——大页在这里是双倍杠杆。KVM 以 KVM_HPAGE_* 家族统一页粒度语言,EPT 项按 fault 逐级建立、按对齐窗口 promote、按 NX 追踪与宿主分裂 demote;宿主 THP 提供弹性大页来源,hugetlbfs 后端提供确定性。客户机侧自身的大页配置(THP/池)决定 GVA 段的杠杆,两段协同才有完整收益;退化链的诊断需要宿主 vmstat、KVM spte 统计与客户机 smaps 的三方对照。本章至此完成大页全景;下一章进入页面置换与回收——LRU、kswapd 与 OOM 的内存压力防线。