6.1 大页与 HMM:THP 和 HugeTLB 的处理

本章为进阶主题,需要理解前面章节的技术,可以选择性阅读。

1. 什么是大页?为什么需要它?

大页 (Huge Page) 是比标准 4 KiB 更大的内存页,常见大小为 2 MiB(PMD 级)或 1 GiB(PUD 级)。

大页大小由 CPU 页表结构决定。x86-64 四级页表每级有 512 个条目:

text 复制代码
PGD → PUD → PMD → PTE → 4 KiB page
       │      │
       │      └─ PMD 直接指向物理页 → 512 × 4 KiB = 2 MiB 大页
       │
       └─ PUD 直接指向物理页 → 512 × 2 MiB = 1 GiB 大页

大页本质是"跳过下级页表,让中间级 entry 直接映射物理内存"。因此大小必须等于该级覆盖的范围,是 2 的幂次方,无法任意指定。

为什么需要大页:

问题 4 KiB 小页 2 MiB 大页
映射 1 GiB 内存 262,144 个 PTE 512 个 PMD entry
TLB 条目消耗 高(易 miss) 低(覆盖范围大)
页表内存开销 ~2 MiB ~4 KiB
page fault 次数

对 GPU/加速器尤其重要:

  • 设备访问模式通常是大范围连续访问
  • 减少设备页表条目 → 更少 GPU TLB miss
  • 减少 DMA 映射开销
  • 迁移时批量处理效率更高

2. HMM 核心策略

HMM 对大页的策略:不拆页,输出 base-page PFN 数组 + order 信息

text 复制代码
CPU 页表:1 个 PMD 覆盖 512 个 4 KiB pages
HMM 输出:512 个 hmm_pfns[] 项,每项带 PFN + flags + map_order

老驱动按 4 KiB 消费;新驱动通过 hmm_pfn_to_map_order() 优化设备页表粒度。


3. 三类大页的区别

类型 页大小 来源 HMM 入口
THP 2 MiB (PMD) MADV_HUGEPAGE / khugepaged hmm_vma_walk_pmd()
PUD leaf 1 GiB (PUD) 架构支持的更大透明映射 hmm_vma_walk_pud()
HugeTLB 2 MiB / 1 GiB (可配置) MAP_HUGETLB / hugetlbfs hmm_vma_walk_hugetlb_entry()

为什么有这三种大页?

  • THP:对应用透明,内核自动合并/拆分,适合通用场景。缺点是不保证分配成功。
  • HugeTLB:需预留内存池,保证分配成功,适合数据库、虚拟机等需要稳定大页的场景。
  • PUD leaf:1 GiB 超大页,适合内存密集型工作负载(如大型 AI 模型),减少更多 TLB miss。

关键差异:

  • THP 可被 split、受回收管理
  • HugeTLB 需预留 pool,独立锁/fault 路径
  • PUD leaf 依赖 CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD

4. HMM 输出格式

PFN 编码(include/linux/hmm.h):

c 复制代码
enum hmm_pfn_flags {
    HMM_PFN_VALID = 1UL << (BITS_PER_LONG - 1),
    HMM_PFN_WRITE = 1UL << (BITS_PER_LONG - 2),
    HMM_PFN_ORDER_SHIFT = (BITS_PER_LONG - 11),
    // ...
};

// 获取映射 order
static inline unsigned int hmm_pfn_to_map_order(unsigned long hmm_pfn) {
    return (hmm_pfn >> HMM_PFN_ORDER_SHIFT) & 0x1F;
}

2 MiB THP 输出示例:

text 复制代码
order = PMD_SHIFT - PAGE_SHIFT = 9  (512 pages)

hmm_pfns[0]   = pfn+0   | VALID | WRITE | order(9)
hmm_pfns[1]   = pfn+1   | VALID | WRITE | order(9)
...
hmm_pfns[511] = pfn+511 | VALID | WRITE | order(9)

驱动消费方式:

  • 保守:逐项按 4 KiB 建设备 PTE
  • 优化:检测 order + 对齐,建立 2 MiB GPU PTE

5. 读取大页的代码路径

PMD THP:hmm_vma_walk_pmd()

c 复制代码
static const struct mm_walk_ops hmm_walk_ops = {
    .pud_entry      = hmm_vma_walk_pud,
    .pmd_entry      = hmm_vma_walk_pmd,
    .hugetlb_entry  = hmm_vma_walk_hugetlb_entry,
    // ...
};

处理流程:

  1. pmdp_get_lockless() 读取 PMD
  2. 迁移中 → 等待或返回空
  3. pmd_trans_huge() → 调用 hmm_vma_handle_pmd()

hmm_vma_handle_pmd() 核心逻辑:

c 复制代码
// 权限转换
cpu_flags = pmd_to_hmm_pfn_flags(range, pmd);

// 需要 fault?
if (hmm_range_need_fault(...))
    return hmm_vma_fault(addr, end, required_fault, walk);

// 逐 base page 填入数组
pfn = pmd_pfn(pmd) + ((addr & ~PMD_MASK) >> PAGE_SHIFT);
for (i = 0; addr < end; addr += PAGE_SIZE, i++, pfn++)
    hmm_pfns[i] = pfn | cpu_flags;

PUD leaf

逻辑与 PMD 类似,order 更大:

c 复制代码
hmm_pfn_flags_order(PUD_SHIFT - PAGE_SHIFT)

HugeTLB:独立路径

c 复制代码
static int hmm_vma_walk_hugetlb_entry(pte_t *pte, ...)
{
    ptl = huge_pte_lock(hstate_vma(vma), walk->mm, pte);
    entry = huge_ptep_get(walk->mm, addr, pte);
    
    // order 来自 hstate,非固定值
    cpu_flags = pte_to_hmm_pfn_flags(range, entry) |
                hmm_pfn_flags_order(huge_page_order(hstate_vma(vma)));
    
    // fault 前必须释放 hugetlb 锁避免死锁
    if (required_fault) {
        spin_unlock(ptl);
        hugetlb_vma_unlock_read(vma);
        ret = hmm_vma_fault(...);
        hugetlb_vma_lock_read(vma);
        return ret;
    }
}

6. 大页迁移

migrate_vma 的 compound 支持

mm/migrate_device.cmigrate_vma_collect_huge_pmd() 尝试按 PMD 粒度迁移:

c 复制代码
if (thp_migration_supported() &&
    (migrate->flags & MIGRATE_VMA_SELECT_COMPOUND) &&
    IS_ALIGNED(start, HPAGE_PMD_SIZE) &&
    IS_ALIGNED(end, HPAGE_PMD_SIZE)) {
    
    migrate->src[migrate->npages] = migrate_pfn(pfn) | write
                                    | MIGRATE_PFN_MIGRATE
                                    | MIGRATE_PFN_COMPOUND;
}

Split Fallback

不满足条件时退回 base page:

  • 范围非 PMD 对齐
  • 调用者未设 MIGRATE_VMA_SELECT_COMPOUND
  • 架构不支持 THP migration
  • folio 锁失败
c 复制代码
fallback:
    ret = split_folio(folio);
    if (ret)
        return migrate_vma_collect_skip(start, end, walk);
    return -ENOENT;  // 回到 PTE 级路径

7. 并发模型

HMM 不锁住页表结构,而是靠 快照 + notifier retry

text 复制代码
1. mmu_interval_read_begin()
2. 读页表,得到 hmm_pfns[]
3. 并发 split/unmap → notifier 更新 seq
4. mmu_interval_read_retry() → seq 变了则重新 fault

关键注释:

No need to take pmd_lock here, even if some other thread is splitting the huge pmd we will get that event through mmu_notifier callback.


8. 驱动消费指南

c 复制代码
// 基本检查
if (!(hmm_pfn & HMM_PFN_VALID)) ...
page = hmm_pfn_to_page(hmm_pfn);

// 大页优化(需额外验证)
order = hmm_pfn_to_map_order(hmm_pfn);

使用 order 前必须确认:

  • VA 和 PFN 按 1 << order 对齐
  • range 覆盖完整大页
  • 后续 PFN 连续、flags 一致
  • 设备页表支持该粒度
  • notifier retry 通过

不满足任何条件 → 退回 base page 映射


9. THP vs HugeTLB 对比

维度 THP HugeTLB
用户入口 MADV_HUGEPAGE MAP_HUGETLB
VMA 标志 普通 VMA VM_HUGETLB
HMM walker pmd_entry / pud_entry hugetlb_entry
order 来源 PMD/PUD_SHIFT - PAGE_SHIFT huge_page_order(hstate)
PMD/PUD 页表锁 huge_pte_lock() + VMA lock
可 split 由 HugeTLB 子系统管理

10. test_hmm 验证

Snapshot 路径把 order 转成 UAPI flag:

c 复制代码
if (hmm_pfn_to_map_order(entry) + PAGE_SHIFT == PMD_SHIFT)
    *perm |= HMM_DMIRROR_PROT_PMD;

测试用例(tools/testing/selftests/mm/hmm-tests.c):

测试 验证点
compound HugeTLB snapshot 权限和 order
migrate_anon_huge_* THP 迁移/回迁
migrate_partial_unmap_fault 部分 unmap 后行为
migrate_anon_huge_err 错误注入和 fallback

附录:关键代码索引

文件 内容
include/linux/hmm.h HMM_PFN_ORDER_SHIFT, hmm_pfn_to_map_order()
mm/hmm.c hmm_vma_walk_pmd/pud(), hmm_vma_walk_hugetlb_entry()
mm/migrate_device.c migrate_vma_collect_huge_pmd(), MIGRATE_PFN_COMPOUND
lib/test_hmm.c order → HMM_DMIRROR_PROT_PMD/PUD 转换

总结

  1. 不主动拆页:HMM 尊重 CPU 页表粒度,读出 PFN + order
  2. 统一输出格式 :按 base page 填 hmm_pfns[]map_order 是优化提示
  3. 迁移优先正确性:尝试 compound 迁移,不满足条件则 split fallback
  4. 大页是快照属性:CPU 随时可能改变粒度,驱动必须通过 notifier 协议处理

下一篇:HMM 与 memcg、userfaultfd、TLB flushing 等子系统的交互。