〇、全景:4KB 页不够用,TLB 先扛不住
默认页大小是 4KB。这对大内存工作负载是灾难:TLB(页表缓存)容量有限 ,一个 TLB 项只覆盖一个页。进程用 1GB 内存 = 262144 个 4KB 页,需要 26 万条 TLB 项------远超 TLB 容量(L1 几十到几百项、L2 几千项),结果就是频繁 TLB miss:每次 miss 都要走一遍多级页表翻译,慢一个数量级。
大页就是让"一个页表项覆盖更多内存":2MB 大页一个项顶 512 个 4KB 页,1GB 大页一个项顶 26 万页。内核有两种大页,机制完全不同:
#mermaid-svg-qbyQW13O2AyYV6rN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qbyQW13O2AyYV6rN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qbyQW13O2AyYV6rN .error-icon{fill:#552222;}#mermaid-svg-qbyQW13O2AyYV6rN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qbyQW13O2AyYV6rN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qbyQW13O2AyYV6rN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qbyQW13O2AyYV6rN .marker.cross{stroke:#333333;}#mermaid-svg-qbyQW13O2AyYV6rN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qbyQW13O2AyYV6rN p{margin:0;}#mermaid-svg-qbyQW13O2AyYV6rN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qbyQW13O2AyYV6rN .cluster-label text{fill:#333;}#mermaid-svg-qbyQW13O2AyYV6rN .cluster-label span{color:#333;}#mermaid-svg-qbyQW13O2AyYV6rN .cluster-label span p{background-color:transparent;}#mermaid-svg-qbyQW13O2AyYV6rN .label text,#mermaid-svg-qbyQW13O2AyYV6rN span{fill:#333;color:#333;}#mermaid-svg-qbyQW13O2AyYV6rN .node rect,#mermaid-svg-qbyQW13O2AyYV6rN .node circle,#mermaid-svg-qbyQW13O2AyYV6rN .node ellipse,#mermaid-svg-qbyQW13O2AyYV6rN .node polygon,#mermaid-svg-qbyQW13O2AyYV6rN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qbyQW13O2AyYV6rN .rough-node .label text,#mermaid-svg-qbyQW13O2AyYV6rN .node .label text,#mermaid-svg-qbyQW13O2AyYV6rN .image-shape .label,#mermaid-svg-qbyQW13O2AyYV6rN .icon-shape .label{text-anchor:middle;}#mermaid-svg-qbyQW13O2AyYV6rN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qbyQW13O2AyYV6rN .rough-node .label,#mermaid-svg-qbyQW13O2AyYV6rN .node .label,#mermaid-svg-qbyQW13O2AyYV6rN .image-shape .label,#mermaid-svg-qbyQW13O2AyYV6rN .icon-shape .label{text-align:center;}#mermaid-svg-qbyQW13O2AyYV6rN .node.clickable{cursor:pointer;}#mermaid-svg-qbyQW13O2AyYV6rN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qbyQW13O2AyYV6rN .arrowheadPath{fill:#333333;}#mermaid-svg-qbyQW13O2AyYV6rN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qbyQW13O2AyYV6rN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qbyQW13O2AyYV6rN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qbyQW13O2AyYV6rN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qbyQW13O2AyYV6rN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qbyQW13O2AyYV6rN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qbyQW13O2AyYV6rN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qbyQW13O2AyYV6rN .cluster text{fill:#333;}#mermaid-svg-qbyQW13O2AyYV6rN .cluster span{color:#333;}#mermaid-svg-qbyQW13O2AyYV6rN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qbyQW13O2AyYV6rN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qbyQW13O2AyYV6rN rect.text{fill:none;stroke-width:0;}#mermaid-svg-qbyQW13O2AyYV6rN .icon-shape,#mermaid-svg-qbyQW13O2AyYV6rN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qbyQW13O2AyYV6rN .icon-shape p,#mermaid-svg-qbyQW13O2AyYV6rN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qbyQW13O2AyYV6rN .icon-shape .label rect,#mermaid-svg-qbyQW13O2AyYV6rN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qbyQW13O2AyYV6rN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qbyQW13O2AyYV6rN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qbyQW13O2AyYV6rN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 大页:一个页表项
管 2MB / 1GB
HugeTLB
显式预留池
THP 透明大页
内核自动
hugetlbfs / MAP_HUGETLB
管理员预先保留
hstate 预留池
缺页时直接分 2MB
do_huge_pmd_anonymous_page
后台合并 4KB→2MB
khugepaged
一句话主线:大页让一个页表项覆盖 2MB/1GB,减少页表层级和 TLB miss;内核两条路------HugeTLB 是"显式预留池"(管理员/用户预先申请、从池子里分配),THP 是"透明大页"(缺页时
do_huge_pmd_anonymous_page直接分 2MB +khugepaged后台把 4KB 页合并成 2MB),两者都在缺页路径上把"分配一页"升级成"分配一个大页"。
一、为什么大页:TLB 覆盖 + 页表层级
两个直接收益:
- TLB 覆盖率暴增:同样 1GB 内存,4KB 页要 262144 个 TLB 项,2MB 页只要 512 项,1GB 页只要 1 项。TLB miss 数量成数量级下降,对访存密集的负载(数据库、HPC、GPU 计算)是实打实的性能。
- 页表层级变浅:4KB 页要完整走 PGD→P4D→PUD→PMD→PTE 五级;2MB 大页到 PMD 就停(PMD 直接指向大页,省了 PTE 一层);1GB 大页到 PUD 就停(省 PMD+PTE 两层)。翻译更快、页表本身也省内存。
代价也明显:内存碎片------要连续 2MB/1GB 物理内存,内存碎成小页后就凑不出来了。这是大页永恒的矛盾(见 §六)。
二、大页的页表层级:PMD / PUD 直接指向大页
大页的核心 trick 是"在页表中间层就停下来,让中间层页表项直接指向大页":
| 大页大小 | 停在哪一层 | 省掉哪些层 | 对应 shift |
|---|---|---|---|
| 4KB(普通页) | PTE | (全走) | PAGE_SHIFT=12 |
| 2MB | PMD | 省 PTE 一层 | PMD_SHIFT=21 |
| 1GB | PUD | 省 PMD+PTE 两层 | PUD_SHIFT=30 |
c
// arch/x86/include/asm/pgtable_64_types.h (v6.6, line 91 / 84)
#define PMD_SHIFT 21 // 2^21 = 2MB
#define PUD_SHIFT 30 // 2^30 = 1GB
于是缺页时,内核会先问"这一层能不能直接给个大页",能就少走几层:
c
// mm/memory.c (v6.6, line 5057) ------ __handle_mm_fault 里
if (pud_none(*vmf.pud) &&
hugepage_vma_check(vma, vm_flags, false, true, true)) {
ret = create_huge_pud(&vmf); // 试着直接建 1GB 大页
if (!(ret & VM_FAULT_FALLBACK))
return ret;
}
// ...
if (pmd_none(*vmf.pmd) &&
hugepage_vma_check(vma, vm_flags, false, true, true)) {
ret = create_huge_pmd(&vmf); // 试着直接建 2MB 大页
if (!(ret & VM_FAULT_FALLBACK))
return ret;
}
hugepage_vma_check(huge_memory.c:74)判断"这个 vma 能不能用大页"(是否对齐、权限、是否被 madvise 禁止等);create_huge_pmd 最终落到 do_huge_pmd_anonymous_page。
三、HugeTLB:显式预留池
HugeTLB 是"显式"的 :管理员先通过内核参数//proc/sysfs 预留一批大页,用户态用 mmap(MAP_HUGETLB) 或挂载 hugetlbfs 显式申请。每种大小的大页对应一个 hstate:
c
// mm/hugetlb.c (v6.6, line 53)
struct hstate hstates[HUGE_MAX_HSTATE]; // 每种大页大小一个 hstate
hstate 维护一个预留池 (free_huge_pages/resv_huge_pages/nr_huge_pages 计数)。hugetlb_init(hugetlb.c:4229,subsys_initcall)启动时把预留的大页建好。用户访问大页时,缺页走 hugetlb_fault(hugetlb.c:6079):
- 从预留池
dequeue一个大页(alloc_hugetlb_folio_vma,hugetlb.c:2515); - 池子不够 → 尝试从 buddy 补(
alloc_buddy_hugetlb_folio,hugetlb.c:2120,1GB 页还要用 CMA/alloc_gigantic_folio); - 填好 PTE/PMD,返回。
HugeTLB 的特点:用户/管理员完全掌控------申请多少、什么时候申请、用多大页,都显式指定。好处是确定性(池子就那么大,不会多给),坏处是要手动管理。
四、THP:透明大页 + khugepaged
THP 是"透明"的:用户什么都不用做,内核自动把连续的 4KB 页合成 2MB 大页。有两条路径:
4.1 缺页时直接分 2MB:do_huge_pmd_anonymous_page
匿名映射(堆、栈、malloc 大块)缺页时,如果满足条件,do_huge_pmd_anonymous_page(huge_memory.c:774)直接分配一个 2MB 页,而不是 4KB:
c
// mm/huge_memory.c (v6.6, line 774)
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
// 读访问 → 用全局共享的 2MB 零页(transparent_hugepage_use_zero_page)
if (!(vmf->flags & FAULT_FLAG_WRITE) && ...) {
zero_page = mm_get_huge_zero_page(vma->vm_mm);
// ...
}
// 写访问 → 真正分配一个 2MB 页(__do_huge_pmd_anonymous_page,:646)
// ...
khugepaged_enter_vma(vma, vma->vm_flags); // 把这个 vma 登记给 khugepaged
}
这和普通匿名页(上一篇 §5.1 的 do_anonymous_page)是同一个逻辑的"2MB 放大版":读用共享零页、写才分配,只不过一次分 2MB。
4.2 后台合并 4KB→2MB:khugepaged
已经用 4KB 页建立的映射,由 khugepaged 内核线程后台扫描、把连续的 4KB 页合并成 2MB:
c
// mm/khugepaged.c (v6.6, line 2484)
static void khugepaged_do_scan(struct collapse_control *cc)
{
// ... 遍历 mm → hpage_collapse_scan_pmd(:1240)
// 扫到一个 PMD 范围内的页都"值得合并"→ collapse_huge_page(:1080)
}
collapse_huge_page(khugepaged.c:1080)把 512 个 4KB 页复制进一个新的 2MB 页、改 PMD 指向它、释放原来的 4KB 页。合并失败(内存不够、页被换出等)就跳过。THP 还支持拆分 :__split_huge_pmd(huge_memory.c:2266)把 2MB 大页拆回 4KB(内存压力、需要回写时)。
THP 的开关:
/sys/kernel/mm/transparent_hugepage/enabled(always/madvise/never)。上游默认是always(mm/Kconfig:831),许多发行版把它改成madvise(只在madvise(MADV_HUGEPAGE)的区域开 THP)。
五、与 GPU DMA 的关系(面试高频)
大页对 GPU 计算尤其重要,核心还是 TLB ,但这次是 IOMMU 的 TLB:
- GPU 通过 DMA 访问主存时,设备地址 → 物理地址的翻译走 IOMMU 页表(和 CPU 的 MMU 页表是两套)。IOMMU 也有 TLB,容量同样有限。
- GPU 做大规模显存拷贝/计算(CUDA
cuMem、训练数据搬运)时,若用 4KB 页,IOMMU TLB miss 会成为瓶颈;用 2MB/1GB 大页,IOMMU 页表项和 TLB miss 数量级下降。 - 所以 GPU 场景(尤其 GPU 直通/虚拟化、大显存 DMA)几乎都要求大页。
(IOMMU 页表与大页的细节见 \[blog-iommu-expansion]。)
六、为什么这样设计(Why 层)
6.1 为什么是"显式 HugeTLB"和"透明 THP"两条路,而不是合一条
因为两类用户需求相反:HugeTLB 要"确定性和控制" (数据库/HPC 希望自己精确控制内存布局、预留多少、绝不 OOM 才用);THP 要"零配置" (普通应用希望自动获得好处、又不用懂大页)。前者给池子和显式 API,后者给全自动 + madvise 微调。"显式 vs 透明"对应"专业用户 vs 普通用户"两种取舍。
6.2 为什么 THP 要"缺页时直接分 2MB",而不是只靠 khugepaged 事后合并
因为事后合并有窗口期 :新分配的页一开始都是 4KB,等 khugepaged 扫到才变 2MB,这期间 TLB 该 miss 还是 miss。缺页时直接分 2MB,让"刚申请的连续内存"第一时间就是大页。两者互补:直接分覆盖"新分配",khugepaged 覆盖"老 4KB 页"。
6.3 为什么大页最大的敌人是"内存碎片"
大页要连续 的 2MB/1GB 物理内存。buddy(#2)虽然努力把空闲页合并成大块,但长期运行后内存会被碎成小页,凑不出连续 2MB。这是大页的固有矛盾:越大越快,也越难凑齐 。所以内核有 khugepaged/内存规整(compaction)/CMA 等手段持续"整理"碎片、为大页腾连续空间。
6.4 为什么读大页也用"共享零页"
和普通匿名页一样(#3 §6.4):程序常 malloc 大块但暂时只读,内容全是 0。让所有进程共享一个 2MB 零页,省下海量内存;一旦要写,再 COW(do_huge_pmd_wp_page,huge_memory.c:1289)复制一份。"零页共享 + 写时复制"对大页同样成立,只是从 4KB 放大到 2MB。
七、与已有博客的交叉引用
| 已有博客 | 本篇覆盖的关系 |
|---|---|
| 内存 #3 页表缺页 | 本篇 §二 的大页是 #3 缺页流程在 PMD/PUD 层的"提前终止";do_huge_pmd_anonymous_page 就是 do_anonymous_page 的 2MB 版 |
| 内存 #2 buddy | 2MB 大页从 buddy 拿 order=9 的连续块(alloc_buddy_hugetlb_folio);但 1GB 大页是 order=18,超出 buddy 的 MAX_ORDER=10,buddy 给不了 ,走 CMA/alloc_contig_pages(alloc_gigantic_folio)------碎片问题正是 buddy 的 order 机制固有矛盾 |
| IOMMU 系列 | 本篇 §五 GPU DMA 用大页减少的是 IOMMU TLB miss,不是 MMU;两套页表各自独立(见 \[blog-iommu-expansion]) |
附:本篇关键源码索引
| 符号 | 位置 |
|---|---|
hstates[HUGE_MAX_HSTATE](每种大页一个 hstate) |
mm/hugetlb.c:53 |
hugetlb_init(预留池初始化) |
mm/hugetlb.c:4229 |
hugetlb_fault(大页缺页处理) |
mm/hugetlb.c:6079 |
hugetlb_reserve_pages / alloc_hugetlb_folio_vma |
mm/hugetlb.c:6703 / :2515 |
alloc_buddy_hugetlb_folio(从 buddy 拿大页) |
mm/hugetlb.c:2120 |
hugepage_vma_check(vma 能否用大页) |
mm/huge_memory.c:74 |
do_huge_pmd_anonymous_page(THP 缺页分 2MB) |
mm/huge_memory.c:774 |
do_huge_pmd_wp_page(大页写时复制) |
mm/huge_memory.c:1289 |
__split_huge_pmd(大页拆回 4KB) |
mm/huge_memory.c:2266 |
khugepaged_do_scan / hpage_collapse_scan_pmd / collapse_huge_page |
mm/khugepaged.c:2484 / :1240 / :1080 |
__handle_mm_fault 里的大页入口(create_huge_pud/pmd) |
mm/memory.c:5057 / :5091 |
PMD_SHIFT(2MB)/ PUD_SHIFT(1GB) |
arch/x86/include/asm/pgtable_64_types.h:91 / :84 |