本章为进阶主题,需要理解前面章节的技术,可以选择性阅读。
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,
// ...
};
处理流程:
pmdp_get_lockless()读取 PMD- 迁移中 → 等待或返回空
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.c 中 migrate_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 转换 |
总结
- 不主动拆页:HMM 尊重 CPU 页表粒度,读出 PFN + order
- 统一输出格式 :按 base page 填
hmm_pfns[],map_order是优化提示 - 迁移优先正确性:尝试 compound 迁移,不满足条件则 split fallback
- 大页是快照属性:CPU 随时可能改变粒度,驱动必须通过 notifier 协议处理
下一篇:HMM 与 memcg、userfaultfd、TLB flushing 等子系统的交互。