巨型页实战
目录
巨型页实战
Hugetlb vs Transparent Huge Page
- HugeTLBFS (显式大页)
- 必须预留
nr_hugepages
,应用程序使用时必须显示用MAP_HUGETLB
。 - 常用于数据库、KVM 等需要确定性的大页场景。
- THP (透明大页)
- 内核自动合并小页 → 大页,不需要应用显式申请,对用户透明。
- 简单易用,但不保证总能分配到大页(可能退化成普通页)。
- 适合通用程序。
标准巨型页(hugetlb)
文件映射
测试代码
用户空间通过mmap
,用 HugeTLB(显式巨型页) 分配一个巨型页。
// gcc -o hugetlb_test hugetlb_test.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#define HUGEPAGE_SIZE (2 * 1024 * 1024) // 2MB
int main() {
const char *filename = "/dev/hugepages/testfile";
int fd = open(filename, O_CREAT | O_RDWR, 0755);
if (fd < 0) {
perror("open");
return 1;
}
// 扩展文件大小到 2MB
if (ftruncate(fd, HUGEPAGE_SIZE) != 0) {
perror("ftruncate");
close(fd);
return 1;
}
// 使用 MAP_HUGETLB 标志显式申请大页
void *addr = mmap(NULL, HUGEPAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_HUGETLB, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
printf("Hugepage mapped at %p\n", addr);
// 往 hugepage 写数据
strcpy((char *)addr, "Hello HugePage!");
printf("Read from hugepage: %s\n", (char *)addr);
// 等待用户按下回车键后继续
printf("Press Enter to unmap and exit...\n");
getchar();
munmap(addr, HUGEPAGE_SIZE);
close(fd);
unlink(filename);
return 0;
}
测试步骤
/proc/sys/vm/nr_hugepages
,巨型页池中标准巨型页的数量,默认值为0,此时无法分配标准巨型页。将它设置为8,echo 8 | sudo tee /proc/sys/vm/nr_hugepages
。
如果不设置nr_hugepages
的话,mmap会失败:
- 标准巨型页是基于
hugetlbfs
文件系统,需要挂载hugetlbfs
文件系统。
sudo ./hugetlb_test
,分配一个巨型页(2M)。
- 测试结果
sudo cat /proc/$(pidof hugetlb_test)/smaps
匿名映射
// gcc -o hugetlb_anon_test hugetlb_anon_test.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#define HUGEPAGE_SIZE (2 * 1024 * 1024) // 默认 2MB hugepage
int main() {
// 直接通过 mmap + MAP_HUGETLB | MAP_ANONYMOUS 分配一个匿名大页
void *addr = mmap(NULL, HUGEPAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}
printf("Anonymous HugeTLB page mapped at %p\n", addr);
// 写入数据
strcpy((char *)addr, "Hello HugeTLB (anonymous)!");
printf("Read back: %s\n", (char *)addr);
// 等待用户按下回车键后继续
printf("Press Enter to unmap and exit...\n");
getchar();
munmap(addr, HUGEPAGE_SIZE);
return 0;
}
测试结果与上面文件映射的hugetlb基本一致,差别只是文件映射的匿名映射。
区别总结
- 匿名 HugeTLB 映射
- 直接用
mmap(MAP_HUGETLB | MAP_ANONYMOUS)
。 - 不需要文件系统(
hugetlbfs
)。 - 但依赖
/proc/sys/vm/nr_hugepages
预留的池子。
- 直接用
- 基于 hugetlbfs 的映射
- 通过
open("/dev/hugepages/xxx") + mmap(MAP_HUGETLB)
。 - 更灵活,适合文件共享场景。
- 通过
透明巨型页 (THP, Transparent Huge Page)
通过以下两个文件,查询THP
的支持情况
/sys/kernel/mm/transparent_hugepage/enabled
/sys/kernel/mm/transparent_hugepage/defrag
测试代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#define SIZE (512 * 4096) // 2MB,对应一个透明大页(512 个 4KB 页)
int main() {
// 分配一块匿名内存
void *addr = mmap(NULL, SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}
// 建议内核尽量使用透明大页
if (madvise(addr, SIZE, MADV_HUGEPAGE) != 0) {
perror("madvise");
munmap(addr, SIZE);
return 1;
}
printf("Anonymous memory mapped at %p\n", addr);
// 写入数据
strcpy((char *)addr, "Hello Transparent HugePage!");
printf("Read from memory: %s\n", (char *)addr);
// 等待用户检查 smaps
printf("Press Enter to exit (check /proc/%d/smaps | grep AnonHugePages before)...\n", getpid());
getchar();
munmap(addr, SIZE);
return 0;
}
测试结果
标准巨型页(HugeTLB)和透明巨型页(THP)的分配机制
从上面的测试结果中,能够验证出HugeTLb和THP分配物理内存的方式是不同的。
1. 普通页 (4KB) 的分配方式
- 使用
mmap(MAP_ANONYMOUS)
时,内核只是建立了 VMA 区域,并不会立即分配物理内存。 - 真正分配物理页是在进程第一次访问该地址时,触发 缺页异常(page fault) → 内核分配一个 4KB 物理页并映射进去。
延迟分配(lazy allocation)。
2. 显式巨型页 (HugeTLB Pages) 的分配方式
情况完全不同:
- 当调用
mmap(NULL, 2*1024*1024, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0);
内核会 在 mmap 阶段就分配整个 2MB 的物理连续大页。
- 这是因为:
- HugeTLB 必须从 预留池(nr_hugepages) 中分配,这个池子本身就是实际的物理内存。
- 内核不能等到缺页时再“凑”一个 2MB 连续块,否则几乎不可能保证可用。
- 为了避免“部分映射”问题,HugeTLB 必须 一次性分配整个大页。
HugeTLB 是立即分配(eager allocation),并且一次性分配整个大页。
3. 透明巨型页 (THP)
- THP 其实是 普通匿名页的延迟分配模型 + 后台合并。
- 初始时还是缺页时分配的 4KB 小页。
- 后台内核线程(
khugepaged
)会尝试把 512 个连续的 4KB 小页合并成 2MB THP。
THP 是按需分配 + 后台合并,不是一次性分配大页。
4. 总结
- 普通页 (4KB):延迟分配,缺页异常时才分配。
- 显式巨型页 (HugeTLB):
mmap
阶段立即分配,且必须一次性分配整个大页。 - 透明巨型页 (THP):最初按 4KB 分配,后台尝试合并成大页。
应用程序调用 mmap()
│
├── 普通页 (4KB)
│ mmap: 只建立 VMA(不分配物理页)
│ ↓
│ 第一次访问地址 → 缺页异常
│ ↓
│ 内核分配 4KB 物理页,并建立页表映射
│
├── 显式巨型页 (HugeTLB, 2MB/1GB)
│ mmap: 立即从 HugeTLB 池中分配整个物理大页 (2MB/1GB)
│ ↓
│ 建立 VMA + 页表映射
│ ↓
│ 第一次访问地址 → 已有物理大页,直接命中
│
└── 透明巨型页 (THP, 默认 2MB)
mmap: 只建立 VMA(不分配物理页)
↓
第一次访问地址 → 缺页异常
↓
内核分配 4KB 小页,建立页表映射
↓
后台 khugepaged 尝试把 512 个 4KB 页合并为 2MB THP
页表是如何支持巨型页的 ?
巨型页( HugeTLB / THP)是 在上层页表项直接映射大页,跳过低层页表,从而直接映射更大范围的物理地址。
1. ARM64 的页表结构
ARM64 支持 4 级页表,虚拟地址空间常见是 48bit VA。每级索引 9 位(512 项):
VA (48 bit)
├─ L0 (PGD, 9bit)
├─ L1 (PUD, 9bit)
├─ L2 (PMD, 9bit)
└─ L3 (PTE, 9bit)
- L3 (PTE) → 指向 4KB/16KB/64KB 的最小页
- L2/L1 也可以直接映射大页(如果设置了 Block entry)
2. ARM64 的页大小支持
ARM64 页表架构支持多种粒度(依赖内核配置):
- 4KB 基础页:支持 2MB、1GB 巨型页
- 16KB 基础页:支持 32MB 巨型页
- 64KB 基础页:支持 512MB 巨型页
可以看出 ARM64 的大页粒度和 基准页大小 相关。
3. 巨型页映射原理
在 ARM64 页表条目里,有两种类型:
- Table entry → 指向下一级页表
- Block entry → 直接映射一块物理地址(用于巨型页)
(a) 普通页 (4KB)
VA → L0 → L1 → L2 → L3 (PTE 指向 4KB)
(b) 2MB 大页 (HugePage)
- 在 L2 页表项,用 Block entry,直接映射 2MB 区域。
VA → L0 → L1 → L2 (Block entry → 2MB 物理页)
© 1GB 大页
- 在 L1 页表项,用 Block entry,直接映射 1GB 区域。
VA → L0 → L1 (Block entry → 1GB 物理页)
4. 硬件工作方式
- MMU 遍历页表时,如果某级是 Table entry → 继续下一层。
- 如果是 Block entry → 直接取物理基址,加上页内偏移,完成翻译。
5. 内核如何用
- 透明大页 (THP):ARM64 内核同样会在 PMD(L2) 层建 2MB Block entry。
- 显式大页 (HugeTLB):用户 mmap hugetlbfs 时,内核会在 L1/L2 页表层直接创建 Block entry。
6. 对性能的好处
- 减少页表层级:大页直接用上层表项,不用再走到 PTE。
- 减少 TLB miss:一个 2MB 页只需 1 个 TLB entry,相比 512 个 4KB 页节省很多。