本文基于 Linux 6.4-rc1(commit ac9a78681b92)源码,所有代码片段均来自真实内核源文件。
一、为什么需要大页?
1.1 TLB Miss 的性能代价
CPU 访问内存经历两步:先查 TLB(Translation Lookaside Buffer)将虚拟地址翻译为物理地址,TLB 命中则直接访存;TLB Miss 时要走多级页表(x86-64 通常是 PGD → P4D → PUD → PMD → PTE),每级都是一次内存读操作,典型情况下一次 TLB Miss 耗费 50 ~ 100 个 CPU 周期,而 L1 Cache 命中只需 4 个周期。
x86-64 的虚拟地址翻译过程:一个 64 位虚拟地址被分割为 [PGD(9 bits)] [P4D(9 bits)] [PUD(9 bits)] [PMD(9 bits)] [PTE(9 bits)] [Offset(12 bits)]。每次缺 TLB 时,硬件 page table walker 依次访问四级页表,最多需要 4 次独立的内存访问(每次可能引发 L1/L2/L3 缓存缺失)。使用 2MB 大页时,翻译在 PMD 层终止,仅需访问 3 级页表;使用 1GB 大页时,在 PUD 层终止,仅需 2 级。
现代服务器 CPU 的 L2 TLB(STLB)通常有 1024 ~ 4096 个条目。以 4KB 页为单位,4096 个 TLB 条目只能覆盖 16 MB 地址空间。对于需要频繁访问数 GB 热数据的数据库引擎、JVM 堆、KVM 客户机内存,TLB Miss 率居高不下,成为主要性能瓶颈之一。
TLB 的组织结构通常分为两级:
- L1 ITLB/DTLB:延迟 1~2 个周期,容量小(4KB 页通常 64 条目,2MB 大页 32 条目)。
- L2 Unified TLB(STLB):延迟约 8 个周期,容量 1024~4096 条目,混合 4KB 和大页条目。
- Page Table Walk:L2 TLB Miss 后触发,延迟 50~100 周期,受内存子系统影响巨大。
值得注意的是,大页在 L1 TLB 中通常独享专用条目,与 4KB 页分开管理,这意味着即便只使用少量大页,也能获得专属的 L1 TLB 保护,效益极高。
实测数据(Intel Xeon 4th Gen,64GB JVM 堆):
| 页大小 |
4KB |
2MB |
1GB |
| 同等 TLB 条目覆盖范围 |
16 MB |
8 GB |
4 TB |
| TLB Miss 率 |
~12% |
~0.3% |
~0% |
| 吞吐量提升 |
基准 |
+18% |
+22% |
1.2 页大小对比
| 页类型 |
大小 |
页表级别 |
arch 支持 |
| 普通页 |
4 KB |
PTE |
所有 |
| 大页(Huge) |
2 MB |
PMD |
x86-64, ARM64 |
| 巨页(Gigantic) |
1 GB |
PUD |
x86-64 |
大页的关键优势在于:1 个 TLB 条目覆盖 2MB,等效于 512 个普通 PTE 条目,TLB 命中率大幅提升。
1.3 适合大页的场景
- 数据库(Oracle、PostgreSQL、MySQL):Buffer Pool / Shared Memory 访问模式高度局部化,适合显式大页(HugeTLBFS)。
- KVM 虚拟机:Host 端为 Guest 物理内存使用大页,EPT(Extended Page Table)条目减少,VM Exit 降低。
- HPC / 科学计算:矩阵运算、FFT 等密集内存访问,THP 即可显著改善。
- Java 应用:JVM 使用
-XX:+UseHugeTLBFS 或 -XX:+UseTransparentHugePages 让 GC 管理的堆使用大页。
二、显式大页(HugeTLBFS)
HugeTLBFS 是 Linux 内核提供的显式大页机制。它的设计思路是:在系统初始化或运行时预先从 buddy 分配器申请若干连续大页,将它们维护在 hstate 的空闲链表中,然后通过一个伪文件系统(hugetlbfs)暴露给用户空间,用户以 mmap/shmget 等标准接口消费。
HugeTLBFS 的整体数据流如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| sysctl vm.nr_hugepages = 1024 │ ▼ set_max_huge_pages(h, 1024) │ ▼ alloc_fresh_hugetlb_folio() ──→ buddy 分配 512 个连续 4KB 页 │ 组成 order-9 compound page ▼ prep_new_hugetlb_folio() ──→ 设置 HUGETLB_PAGE_DTOR,加入 hstate │ ▼ hstate.hugepage_freelists[nid] ← 空闲大页链表
用户 mmap(MAP_HUGETLB) │ ▼ hugetlb_reserve_pages() ──→ 从空闲计数中预留,更新 resv_huge_pages │ ▼ 缺页 hugetlb_fault() ──→ 从 freelist 摘取大页,填写 PTE(PMD 级)
|
2.1 struct hstate:大页池管理核心
内核为每种大页尺寸维护一个 struct hstate,定义在 include/linux/hugetlb.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #define HSTATE_NAME_LEN 32
struct hstate { struct mutex resize_lock; int next_nid_to_alloc; int next_nid_to_free; unsigned int order; unsigned int demote_order; unsigned long mask; unsigned long max_huge_pages; 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]; };
|
字段含义:
nr_huge_pages:全局大页池的页面总数(/proc/meminfo 中的 HugePages_Total)。
free_huge_pages:当前可分配数(HugePages_Free)。
resv_huge_pages:已经通过 mmap(MAP_HUGETLB) 预留但尚未发生缺页的数量(HugePages_Rsvd)。
surplus_huge_pages:在 max_huge_pages 基础上超额从 buddy 系统临时借用的页数(HugePages_Surp)。
hugepage_freelists:按 NUMA 节点组织的空闲链表,优先从本节点分配大页以降低跨节点访问代价。
全局数组 hstates[] 保存所有注册的 hstate(mm/hugetlb.c 第 52 行):
1
| struct hstate hstates[HUGE_MAX_HSTATE];
|
系统中可以同时存在多个 hstate,每种大页尺寸(2MB、1GB)对应一个。default_hstate 是默认的 2MB 大页,用户通过 /proc/sys/vm/nr_hugepages 控制的就是这个默认 hstate。hugepage_subpool 是文件系统级别的子池,对 hugetlbfs 挂载点设置 size 和 min_size 参数时,子池负责在全局池和文件系统之间进行二次分配和配额管理。
resv_map 与 file_region 一起记录哪些页偏移范围已经预留了大页。对于共享映射(如 shmget + SHM_HUGETLB),resv_map 挂在 inode 上,多个映射共享同一 resv_map;对于私有映射(MAP_PRIVATE | MAP_HUGETLB),每个 VMA 拥有独立的 resv_map,确保 COW 语义下预留计数正确。
2.2 alloc_fresh_hugetlb_folio:从 buddy 分配 compound page
当大页池需要扩充时,调用 alloc_fresh_hugetlb_folio(mm/hugetlb.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| static struct folio *alloc_fresh_hugetlb_folio(struct hstate *h, gfp_t gfp_mask, int nid, nodemask_t *nmask, nodemask_t *node_alloc_noretry) { struct folio *folio; bool retry = false;
retry: if (hstate_is_gigantic(h)) folio = alloc_gigantic_folio(h, gfp_mask, nid, nmask); else folio = alloc_buddy_hugetlb_folio(h, gfp_mask, nid, nmask, node_alloc_noretry); if (!folio) return NULL; if (hstate_is_gigantic(h)) { if (!prep_compound_gigantic_folio(folio, huge_page_order(h))) { free_gigantic_folio(folio, huge_page_order(h)); if (!retry) { retry = true; goto retry; } return NULL; } } prep_new_hugetlb_folio(h, folio, folio_nid(folio)); return folio; }
|
对于 2MB 大页(非 gigantic),调用 alloc_buddy_hugetlb_folio,其核心是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static struct folio *alloc_buddy_hugetlb_folio(struct hstate *h, gfp_t gfp_mask, int nid, nodemask_t *nmask, nodemask_t *node_alloc_noretry) { int order = huge_page_order(h); struct page *page; ... gfp_mask |= __GFP_COMP|__GFP_NOWARN; if (alloc_try_hard) gfp_mask |= __GFP_RETRY_MAYFAIL; ... page = __alloc_pages(gfp_mask, order, nid, nmask); ... __count_vm_event(HTLB_BUDDY_PGALLOC); return page_folio(page); }
|
关键点:
order = 9:从 buddy 分配 2^9 = 512 个连续物理页(2MB)。
__GFP_COMP:将 512 个页面组成一个 compound page(复合页),head page 的 compound_order 设为 9,tail pages 指向 head。
- 分配后调用
prep_new_hugetlb_folio 设置析构函数(HUGETLB_PAGE_DTOR)并更新 hstate 计数器。
2.3 hugetlb_fault:大页缺页处理
用户访问 MAP_HUGETLB 区域触发缺页时,handle_mm_fault 会识别到该 VMA 对应 HugeTLBFS 并调用 hugetlb_fault(mm/hugetlb.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| vm_fault_t hugetlb_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, unsigned int flags) { pte_t *ptep, entry; spinlock_t *ptl; vm_fault_t ret; u32 hash; pgoff_t idx; struct page *page = NULL; struct hstate *h = hstate_vma(vma); ... mapping = vma->vm_file->f_mapping; idx = vma_hugecache_offset(h, vma, haddr); hash = hugetlb_fault_mutex_hash(mapping, idx); mutex_lock(&hugetlb_fault_mutex_table[hash]);
hugetlb_vma_lock_read(vma); ptep = huge_pte_alloc(mm, vma, haddr, huge_page_size(h)); if (!ptep) { hugetlb_vma_unlock_read(vma); mutex_unlock(&hugetlb_fault_mutex_table[hash]); return VM_FAULT_OOM; }
entry = huge_ptep_get(ptep); if (huge_pte_none_mostly(entry)) return hugetlb_no_page(mm, vma, mapping, idx, address, ptep, entry, flags); ... }
|
hugetlb_fault 使用 per-page 互斥锁(hugetlb_fault_mutex_table,4096 个桶的哈希表)序列化对同一大页的并发缺页,避免重复分配。PTE 为空时转入 hugetlb_no_page 完成实际的页分配、页表填充工作。
2.4 hugetlb_reserve_pages:mmap 时的预留
调用 mmap(MAP_HUGETLB) 时,内核并不立即分配物理大页,而是先预留:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| bool hugetlb_reserve_pages(struct inode *inode, long from, long to, struct vm_area_struct *vma, vm_flags_t vm_flags) { long chg = -1, add = -1; struct hstate *h = hstate_inode(inode); struct hugepage_subpool *spool = subpool_inode(inode); struct resv_map *resv_map; ... if (vm_flags & VM_NORESERVE) return true;
if (!vma || vma->vm_flags & VM_MAYSHARE) { resv_map = inode_resv_map(inode); chg = region_chg(resv_map, from, to, ®ions_needed); } else { resv_map = resv_map_alloc(); ... chg = to - from; set_vma_resv_map(vma, resv_map); set_vma_resv_flags(vma, HPAGE_RESV_OWNER); } ... }
|
预留机制确保 mmap 成功即意味着将来的缺页一定能得到大页,避免在运行时因大页不足而 OOM。hstate.resv_huge_pages 记录当前预留数量,与 free_huge_pages 共同决定是否还能新增预留。
可分配的大页数量判断逻辑:当 free_huge_pages - resv_huge_pages > 0 时可以新增预留;当全局大页池耗尽但配置了 nr_overcommit_hugepages 时,允许临时从 buddy 分配额外的 surplus 大页(surplus_huge_pages),一旦使用完毕后归还 buddy 而不是放回大页池。这种 overcommit 机制可以平滑应对短时的大页需求峰值。
2.5 应用层使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <sys/mman.h> #include <stdio.h> #include <string.h>
#define HUGE_SIZE (2UL * 1024 * 1024) #define MAP_HUGE_2MB (21 << MAP_HUGE_SHIFT)
int main(void) { void *p = mmap(NULL, HUGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB, -1, 0); if (p == MAP_FAILED) { perror("mmap"); return 1; } memset(p, 0x42, HUGE_SIZE); printf("addr = %p\n", p); munmap(p, HUGE_SIZE); return 0; }
|
通过 HugeTLBFS 挂载点也可以使用文件方式访问大页:
1 2 3
| mkdir -p /mnt/hugepages mount -t hugetlbfs nodev /mnt/hugepages
|
三、透明大页(THP)
3.1 工作原理
THP(Transparent Huge Pages)让内核无需应用修改就能自动使用 2MB 大页。其核心思想是:在缺页时直接分配 2MB PMD 大页;在进程运行期间,khugepaged 后台守护线程扫描已存在的 4KB 页,尝试将 512 个连续物理页合并(collapse)成一个 2MB 大页。
THP 与 HugeTLBFS 最本质的区别在于管理主体:HugeTLBFS 由用户空间显式控制大页的申请和使用;THP 完全由内核透明管理,用户程序无需感知大页的存在。这种透明性带来了极大的易用性,但也引入了新的复杂性——内核必须在合适的时机自动拆分和合并大页,而这些操作可能在应用程序的关键路径上产生意料之外的开销。
THP 支持两种内存类型:
- 匿名内存(Anonymous):
mmap(MAP_ANONYMOUS) 或进程堆/栈,是 THP 最主要的使用场景。
- 共享内存(Shmem/tmpfs):
/dev/shm、memfd_create 等,需要单独配置 /sys/kernel/mm/transparent_hugepage/shmem_enabled。
文件背景内存(page cache)目前不支持 THP,因为文件系统的块 I/O 和页缓存管理对 2MB 粒度有较高的复杂度要求。
THP 的标志字变量(mm/huge_memory.c 第 57 行):
1 2 3 4 5 6 7 8 9 10
| unsigned long transparent_hugepage_flags __read_mostly = #ifdef CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS (1<<TRANSPARENT_HUGEPAGE_FLAG)| #endif #ifdef CONFIG_TRANSPARENT_HUGEPAGE_MADVISE (1<<TRANSPARENT_HUGEPAGE_REQ_MADV_FLAG)| #endif (1<<TRANSPARENT_HUGEPAGE_DEFRAG_REQ_MADV_FLAG)| (1<<TRANSPARENT_HUGEPAGE_DEFRAG_KHUGEPAGED_FLAG)| (1<<TRANSPARENT_HUGEPAGE_USE_ZERO_PAGE_FLAG);
|
3.2 do_huge_pmd_anonymous_page:THP 缺页路径
当匿名映射发生缺页,且 VMA 满足 THP 条件(大小 >= 2MB、地址对齐)时,handle_mm_fault 走 do_huge_pmd_anonymous_page(mm/huge_memory.c):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf) { struct vm_area_struct *vma = vmf->vma; gfp_t gfp; struct folio *folio; unsigned long haddr = vmf->address & HPAGE_PMD_MASK;
if (!transhuge_vma_suitable(vma, haddr)) return VM_FAULT_FALLBACK; if (unlikely(anon_vma_prepare(vma))) return VM_FAULT_OOM; khugepaged_enter_vma(vma, vma->vm_flags);
if (!(vmf->flags & FAULT_FLAG_WRITE) && !mm_forbids_zeropage(vma->vm_mm) && transparent_hugepage_use_zero_page()) { ... set_huge_zero_page(pgtable, vma->vm_mm, vma, haddr, vmf->pmd, zero_page); ... return ret; }
gfp = vma_thp_gfp_mask(vma); folio = vma_alloc_folio(gfp, HPAGE_PMD_ORDER, vma, haddr, true); if (unlikely(!folio)) { count_vm_event(THP_FAULT_FALLBACK); return VM_FAULT_FALLBACK; } return __do_huge_pmd_anonymous_page(vmf, &folio->page, gfp); }
|
HPAGE_PMD_ORDER = 9,vma_alloc_folio 从 buddy 分配 512 页连续物理内存。分配失败时以 VM_FAULT_FALLBACK 回退,内核继续处理 4KB 缺页,保证应用程序正常运行。
3.3 __do_huge_pmd_anonymous_page:填写 PMD 页表项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| static vm_fault_t __do_huge_pmd_anonymous_page(struct vm_fault *vmf, struct page *page, gfp_t gfp) { struct vm_area_struct *vma = vmf->vma; struct folio *folio = page_folio(page); pgtable_t pgtable; unsigned long haddr = vmf->address & HPAGE_PMD_MASK; vm_fault_t ret = 0;
if (mem_cgroup_charge(folio, vma->vm_mm, gfp)) { folio_put(folio); count_vm_event(THP_FAULT_FALLBACK_CHARGE); return VM_FAULT_FALLBACK; }
pgtable = pte_alloc_one(vma->vm_mm); ... clear_huge_page(page, vmf->address, HPAGE_PMD_NR); __folio_mark_uptodate(folio);
vmf->ptl = pmd_lock(vma->vm_mm, vmf->pmd); if (unlikely(!pmd_none(*vmf->pmd))) { goto unlock_release; } else { pmd_t entry; ... entry = mk_huge_pmd(page, vma->vm_page_prot); entry = maybe_pmd_mkwrite(pmd_mkdirty(entry), vma); folio_add_new_anon_rmap(folio, vma, haddr); folio_add_lru_vma(folio, vma); pgtable_trans_huge_deposit(vma->vm_mm, vmf->pmd, pgtable); set_pmd_at(vma->vm_mm, haddr, vmf->pmd, entry); update_mmu_cache_pmd(vma, vmf->address, vmf->pmd); add_mm_counter(vma->vm_mm, MM_ANONPAGES, HPAGE_PMD_NR); mm_inc_nr_ptes(vma->vm_mm); spin_unlock(vmf->ptl); count_vm_event(THP_FAULT_ALLOC); } return 0; ... }
|
关键实现细节:
mk_huge_pmd 在 PMD 表项中设置 _PAGE_PSE(Page Size Extension)位,告知 MMU 此 PMD 直接映射 2MB 物理页,不再向下走 PTE 级。
pgtable_trans_huge_deposit 把预先分配的 PTE 页表页”存入” PMD 旁,为未来 COW 拆分时复用。
set_pmd_at 是一个内存屏障写,确保在 TLB 更新前物理页内容(clear_huge_page 的零化)对所有 CPU 可见。
3.4 THP 的分裂(split_huge_page)
当需要对 THP 的一部分进行操作(如 munmap 非 2MB 对齐区域、部分 mprotect、发生 ptrace、被 KSM 扫描、内存迁移)时,必须先将 2MB THP 拆回 512 个 4KB 页。THP 的拆分分为两个层面:
- PMD 级拆分(
__split_huge_pmd):只修改页表,将 PMD 大页表项拆成 512 个 PTE,物理页内存布局不变,compound page 继续存在。
- 物理页级拆分(
split_huge_page_to_list):将 compound page 拆分为 512 个独立的 4KB struct page,更新 rmap、LRU、引用计数,物理内存组织发生实质变化。
两种拆分通常配合使用:先 PMD 级拆分解除大页表项,再视需要决定是否进行物理页级拆分。
入口函数 split_huge_page_to_list(mm/huge_memory.c,第 2637 行):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int split_huge_page_to_list(struct page *page, struct list_head *list) { struct folio *folio = page_folio(page); ... VM_BUG_ON_FOLIO(!folio_test_locked(folio), folio); VM_BUG_ON_FOLIO(!folio_test_large(folio), folio);
if (folio_test_anon(folio)) { anon_vma = folio_get_anon_vma(folio); anon_vma_lock_write(anon_vma); } else { mapping = folio->mapping; ... } ... __split_huge_page(page, list, end); ... }
|
__split_huge_page 将 compound page 的每个 tail page 重新初始化为独立的 4KB 页,依次更新 rmap、LRU 链表、引用计数,最后调用 __split_huge_page_tail 处理每个 tail page。
3.5 __split_huge_pmd:PMD 级拆分
在 COW 或 munmap 时,还需要在页表层面将 PMD 大页表项拆成 512 个 PTE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| void __split_huge_pmd(struct vm_area_struct *vma, pmd_t *pmd, unsigned long address, bool freeze, struct folio *folio) { spinlock_t *ptl; struct mmu_notifier_range range;
mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm, address & HPAGE_PMD_MASK, (address & HPAGE_PMD_MASK) + HPAGE_PMD_SIZE); mmu_notifier_invalidate_range_start(&range); ptl = pmd_lock(vma->vm_mm, pmd);
if (pmd_trans_huge(*pmd) || pmd_devmap(*pmd) || is_pmd_migration_entry(*pmd)) { if (folio && folio != page_folio(pmd_page(*pmd))) goto out; __split_huge_pmd_locked(vma, pmd, range.start, freeze); } out: spin_unlock(ptl); mmu_notifier_invalidate_range_only_end(&range); }
|
__split_huge_pmd_locked 从 PMD 的 “deposit” 中取出预存的 PTE 页表页,填充 512 个 PTE 条目后将 PMD 表项替换为指向该 PTE 页表页的普通 PMD 指针,同时刷新 TLB。
四、THP 的 COW 处理
4.1 do_huge_pmd_wp_page:THP 写时复制
当父进程 fork() 后子进程写入 THP 映射区域,触发写保护缺页:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| vm_fault_t do_huge_pmd_wp_page(struct vm_fault *vmf) { const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE; struct vm_area_struct *vma = vmf->vma; struct folio *folio; struct page *page; unsigned long haddr = vmf->address & HPAGE_PMD_MASK; pmd_t orig_pmd = vmf->orig_pmd;
vmf->ptl = pmd_lockptr(vma->vm_mm, vmf->pmd); ... spin_lock(vmf->ptl); ... page = pmd_page(orig_pmd); folio = page_folio(page);
if (PageAnonExclusive(page)) goto reuse;
... if (folio_ref_count(folio) == 1) { pmd_t entry; page_move_anon_rmap(page, vma); folio_unlock(folio); reuse: if (unlikely(unshare)) { spin_unlock(vmf->ptl); return 0; } entry = pmd_mkyoung(orig_pmd); entry = maybe_pmd_mkwrite(pmd_mkdirty(entry), vma); if (pmdp_set_access_flags(vma, haddr, vmf->pmd, entry, 1)) update_mmu_cache_pmd(vma, vmf->address, vmf->pmd); spin_unlock(vmf->ptl); return 0; }
unlock_fallback: folio_unlock(folio); spin_unlock(vmf->ptl); fallback: __split_huge_pmd(vma, vmf->pmd, vmf->address, false, NULL); return VM_FAULT_FALLBACK; }
|
COW 处理有两条路径:
- 快速路径(reuse):若 THP 无共享(引用计数为 1 或
PageAnonExclusive),直接将 PMD 标为 dirty + writable,无需复制,O(1) 完成。
- 慢速路径(fallback):存在共享时,调用
__split_huge_pmd 将 2MB PMD 拆成 512 个 PTE,然后返回 VM_FAULT_FALLBACK,由上层按 4KB 粒度完成 COW 复制,只复制实际被写的那 1 个 4KB 页。
这是 THP 相比 HugeTLBFS 的一个重要差异:HugeTLBFS 的 COW 必须复制整个 2MB 页(代价高昂),而 THP COW 可以回退到 4KB 粒度,只复制被写的页,节省 511 个页的内存复制开销。
fork() 后的 THP 生命周期:父进程调用 fork() 时,子进程以写保护方式共享父进程的 THP,PMD 表项标记为只读。当任意一方发生写操作时,触发 do_huge_pmd_wp_page:若此时引用计数为 1(另一方已经退出),走快速路径直接解除写保护;若双方均存在,则拆分 PMD 并按 4KB 粒度 COW。这套机制使得 fork() + exec() 的典型模式(子进程很快 exec)不会引发大页整体复制,性能开销与 4KB 页一致。
五、khugepaged:后台大页合并
5.1 守护线程结构
khugepaged 是内核专用的后台线程,负责将已存在的 4KB 页合并为 2MB THP。它维护一个全局扫描游标:
1 2 3 4 5 6
| struct khugepaged_scan { struct list_head mm_head; struct khugepaged_mm_slot *mm_slot; unsigned long address; };
|
主要调优参数(均可通过 sysfs 配置):
1 2 3 4 5 6 7 8 9
|
static unsigned int khugepaged_pages_to_scan __read_mostly; static unsigned int khugepaged_scan_sleep_millisecs __read_mostly = 10000;
static unsigned int khugepaged_alloc_sleep_millisecs __read_mostly = 60000; static unsigned int khugepaged_max_ptes_none __read_mostly; static unsigned int khugepaged_max_ptes_swap __read_mostly; static unsigned int khugepaged_max_ptes_shared __read_mostly;
|
5.2 khugepaged_scan_mm_slot:扫描 mm 槽位
每当进程调用 mmap 创建新的匿名 VMA 且满足 THP 条件时,khugepaged_enter_vma(由 do_huge_pmd_anonymous_page 调用,见上文 mm/huge_memory.c 第 790 行)会将该进程的 mm_struct 注册到 khugepaged_scan.mm_head 链表。khugepaged 线程从该链表轮询,依次扫描每个 mm 中的 VMA。
khugepaged_do_scan 循环调用 khugepaged_scan_mm_slot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| static unsigned int khugepaged_scan_mm_slot(unsigned int pages, int *result, struct collapse_control *cc) { struct vma_iterator vmi; struct khugepaged_mm_slot *mm_slot; struct mm_struct *mm; struct vm_area_struct *vma; int progress = 0; ... mm = slot->mm; if (unlikely(!mmap_read_trylock(mm))) goto breakouterloop_mmap_lock;
vma_iter_init(&vmi, mm, khugepaged_scan.address); for_each_vma(vmi, vma) { ... if (!hugepage_vma_check(vma, vma->vm_flags, false, false, true)) goto skip;
hstart = round_up(vma->vm_start, HPAGE_PMD_SIZE); hend = round_down(vma->vm_end, HPAGE_PMD_SIZE); ... while (khugepaged_scan.address < hend) { ... *result = hpage_collapse_scan_pmd(mm, vma, khugepaged_scan.address, &mmap_locked, cc); ... } } ... }
|
5.3 hpage_collapse_scan_pmd:检查 PMD 区域是否可合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| static int hpage_collapse_scan_pmd(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, bool *mmap_locked, struct collapse_control *cc) { pmd_t *pmd; pte_t *pte, *_pte; int result = SCAN_FAIL, referenced = 0; int none_or_zero = 0, shared = 0; ... pte = pte_offset_map_lock(mm, pmd, address, &ptl); for (_address = address, _pte = pte; _pte < pte + HPAGE_PMD_NR; _pte++, _address += PAGE_SIZE) { pte_t pteval = *_pte; if (is_swap_pte(pteval)) { ++unmapped; if (!cc->is_khugepaged || unmapped <= khugepaged_max_ptes_swap) { continue; } else { result = SCAN_EXCEED_SWAP_PTE; goto out_unmap; } } if (pte_none(pteval) || is_zero_pfn(pte_pfn(pteval))) { ++none_or_zero; if (!userfaultfd_armed(vma) && (!cc->is_khugepaged || none_or_zero <= khugepaged_max_ptes_none)) { continue; } else { result = SCAN_EXCEED_NONE_PTE; goto out_unmap; } } ... if (pte_write(pteval)) writable = true; ... } ... }
|
扫描逻辑:
- 逐一检查 512 个 PTE,允许一定数量的 swap 页(
khugepaged_max_ptes_swap)和空页(khugepaged_max_ptes_none)存在。
- 若 PTE 带有
uffd-wp(userfaultfd 写保护)则放弃合并。
- 通过检查后,调用
collapse_huge_page 分配新 2MB 大页,将 512 个 4KB 页的内容复制进去,替换 PMD 表项。
5.4 collapse_huge_page:实际合并流程
hpage_collapse_scan_pmd 检查通过后,由 collapse_huge_page(mm/khugepaged.c 第 1079 行)完成实际的合并操作:
- 分配新 2MB 大页:调用
alloc_charge_hpage 从 buddy 分配 order-9 页面。
- 隔离 512 个 4KB 页:调用
__collapse_huge_page_isolate,逐页从 LRU 链表摘除,检查引用计数,处于 swap 中的页面执行 swapin(__collapse_huge_page_swapin)。
- 复制内容:
__collapse_huge_page_copy 将 512 个 4KB 页的内容逐页复制进新的 2MB folio。
- 替换页表:在
mmap_write_lock 保护下,用一条 PMD 大页表项替换原来的 512 个 PTE,刷新 TLB。
- 释放旧 4KB 页:原来的 512 个 4KB 物理页引用计数归零后归还 buddy。
整个合并过程需要持有目标 mm 的 mmap_write_lock,因此对应用程序有短暂的阻塞影响(通常微秒级)。这也是为什么高延迟敏感场景建议关闭 khugepaged 的主要原因——不可预知的合并时机会引入随机延迟。
六、THP 控制参数
6.1 sysfs 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
|
transparent_hugepage_flags 中各比特位对应 defrag 策略(mm/huge_memory.c 第 738 行 vma_thp_gfp_mask):不同 defrag 模式会向 vma_alloc_folio 传递不同的 gfp 标志,控制内存分配器的压缩行为。
6.2 madvise 精细控制
1 2 3 4 5
| madvise(addr, len, MADV_HUGEPAGE);
madvise(addr, len, MADV_NOHUGEPAGE);
|
在数据库场景中,通常对 Buffer Pool 使用 MADV_HUGEPAGE,对其他小内存结构使用 MADV_NOHUGEPAGE 避免碎片化。
6.3 khugepaged 调优参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| echo 4096 > /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
echo 10000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
echo 60000 > /sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
echo 511 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap
|
七、HugePage 在数据库中的实践
7.1 显式大页配置
系统配置(持久化):
1 2 3 4 5 6 7
| vm.nr_hugepages = 1024 vm.nr_overcommit_hugepages = 128
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
|
PostgreSQL 配置:
1 2 3
| huge_pages = on shared_buffers = 2GB
|
PostgreSQL 在 shmget/mmap 共享内存时会优先传递 SHM_HUGETLB 标志;若大页不足则回退普通页(huge_pages = try)或直接报错退出(huge_pages = on)。
Oracle 数据库:
1 2 3 4
| oracle soft memlock unlimited oracle hard memlock unlimited
|
MySQL InnoDB(MariaDB 10.5+):
1 2 3
| large_pages = ON innodb_buffer_pool_size = 16G
|
7.2 KVM 虚拟机使用大页
libvirt 配置(XML):
1 2 3 4 5 6
| <memoryBacking> <hugepages> <page size="2048" unit="KiB" nodeset="0-1"/> </hugepages> <locked/> </memoryBacking>
|
QEMU 会对 Guest RAM 执行 mmap(MAP_HUGETLB),EPT(Extended Page Table)中的 Level-2(对应 Host PMD)条目直接为 2MB 大页,减少 EPT 走表层级,VM Exit 频率可降低 10% ~ 30%。
7.3 THP 对数据库的负面影响
THP 对数据库工作负载有几个典型负面效应:
- 延迟抖动(Latency Spikes):
khugepaged 合并时会短暂持有 anon_vma_lock_write,高并发场景下与应用线程竞争,产生几毫秒的随机延迟。
- 内存碎片化加剧:THP 需要连续 2MB 物理内存,频繁分配/释放后系统碎片化导致 THP 分配失败,触发同步内存压缩(
defrag=always 时),造成长时间停顿。
fork() COW 放大:若 THP 被多进程共享(如 PostgreSQL 的 fork() 后 worker),COW 时即便只修改 1 个字节也需拆分整个 2MB THP 的页表,增加额外开销。
数据库推荐配置:
1 2 3 4 5 6 7 8 9 10 11
| echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag
echo 0 > /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
|
开机自动禁用(systemd):
1 2 3 4 5 6 7 8 9 10 11
| [Unit] Description=Disable Transparent Huge Pages
[Service] Type=oneshot ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled" ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/defrag"
[Install] WantedBy=multi-user.target
|
八、诊断与性能分析
8.1 /proc/meminfo 大页字段
1
| grep -i huge /proc/meminfo
|
输出示例:
1 2 3 4 5 6 7 8 9 10 11
| AnonHugePages: 614400 kB # THP 匿名大页(单位 KB,614400/2048 = 300 个 2MB THP) ShmemHugePages: 0 kB # shmem/tmpfs THP ShmemPmdMapped: 0 kB FileHugePages: 0 kB FilePmdMapped: 0 kB HugePages_Total: 1024 # HugeTLBFS 总大页数 HugePages_Free: 768 # 空闲大页数 HugePages_Rsvd: 64 # 已预留但未分配(mmap 已预留,缺页未触发) HugePages_Surp: 0 # surplus 超额大页(临时从 buddy 借用) Hugepagesize: 2048 kB Hugetlb: 2097152 kB # HugeTLBFS 总占用内存
|
8.2 /proc/PID/smaps 进程级诊断
1
| cat /proc/$(pgrep postgres | head -1)/smaps | grep -A20 "heap"
|
关键字段:
1 2 3 4
| 7f8000000000-7f8080000000 rw-p ... [heap] Size: 524288 kB AnonHugePages: 520192 kB # 该 VMA 中使用了多少 THP THPeligible: 1 # 该区域是否满足 THP 条件(1=是)
|
8.3 /proc/sys/vm/nr_hugepages 动态调整
1 2 3 4 5 6 7 8
| cat /proc/sys/vm/nr_hugepages
echo 2048 > /proc/sys/vm/nr_hugepages
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
|
注意:大页预留应在系统启动早期(内存碎片化程度低时)完成;运行中的系统若内存碎片严重,增加 nr_hugepages 可能只能部分满足。
8.4 numastat 查看 per-NUMA 大页统计
输出:
1 2 3 4 5
| Node 0 Node 1 Total --------------- --------------- --------------- HugePages_Total 512.00 512.00 1024.00 HugePages_Free 384.00 384.00 768.00 HugePages_Surp 0.00 0.00 0.00
|
NUMA 不均衡时,跨节点大页分配会增加内存访问延迟,应确保大页按应用所在 NUMA 节点均匀分配。
8.5 bpftrace 追踪 THP 分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| bpftrace -e ' kprobe:do_huge_pmd_anonymous_page { @[comm] = count(); } interval:s:5 { print(@); clear(@); } '
bpftrace -e ' tracepoint:huge_memory:mm_khugepaged_scan_pmd_entry { printf("khugepaged scan: %s pid=%d addr=%lx\n", comm, pid, args->address); }'
bpftrace -e ' tracepoint:huge_memory:mm_anon_huge_fault_alloc { @thp_alloc[comm] = count(); } tracepoint:huge_memory:mm_anon_huge_fault_fallback { @thp_fallback[comm] = count(); } END { print(@thp_alloc); print(@thp_fallback); }'
|
也可通过 /proc/vmstat 快速获取系统级 THP 统计:
1
| grep -i thp /proc/vmstat
|
1 2 3 4 5
| thp_fault_alloc 45231 # THP 缺页分配成功次数 thp_fault_fallback 3021 # THP 分配失败回退到 4KB 次数 thp_collapse_alloc 1284 # khugepaged 合并成功次数 thp_collapse_alloc_failed 87 # khugepaged 合并失败次数 thp_split_page 342 # THP 被拆分次数
|
thp_fault_fallback 占比高说明物理内存碎片严重,应考虑调整 defrag 策略或提前预留大页;thp_split_page 过高说明存在频繁 COW 或 munmap 非对齐区域,应排查应用的内存使用模式。
九、总结
Linux 大页机制从两个维度解决 TLB Miss 问题:
| 特性 |
HugeTLBFS(显式大页) |
THP(透明大页) |
| 使用方式 |
需应用显式调用 MAP_HUGETLB |
内核自动,应用无感知 |
| 支持大小 |
2MB、1GB |
2MB(匿名/shmem) |
| 内存锁定 |
预留后不可回收 |
可被拆分、换出 |
| COW 代价 |
复制整个 2MB |
回退 4KB,只复制被写的页 |
| 碎片影响 |
需启动时预留(影响小) |
运行时分配,受碎片影响大 |
| 适用场景 |
数据库 Buffer、KVM Guest RAM |
HPC、JVM、通用服务 |
| 调优难度 |
中(需规划容量) |
高(defrag/khugepaged 参数) |
对于延迟敏感的数据库(Oracle、PostgreSQL),推荐:
- 使用 HugeTLBFS(显式 2MB 大页)承载 Buffer Pool/SGA。
- 关闭 THP 或至少关闭
khugepaged(pages_to_scan=0),消除随机延迟抖动。
对于吞吐优先的 HPC、大内存 Java 应用,推荐:
- 开启 THP(
enabled=always 或 enabled=madvise)。
defrag=defer+madvise:避免同步内存压缩阻塞缺页路径。
- 适当增大
khugepaged/pages_to_scan,加快后台合并速度。
参考源文件:
mm/hugetlb.c
mm/huge_memory.c
mm/khugepaged.c
include/linux/hugetlb.h
延伸阅读: