目录

巨型页实战

巨型页实战

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;
}

测试步骤

  1. /proc/sys/vm/nr_hugepages,巨型页池中标准巨型页的数量,默认值为0,此时无法分配标准巨型页。将它设置为8,echo 8 | sudo tee /proc/sys/vm/nr_hugepages

https://i-blog.csdnimg.cn/img_convert/25fd00053ddbfb7c8a9acf5998909452.png

https://i-blog.csdnimg.cn/img_convert/3912d077953de969e5b3b6f5a4933a2d.png

如果不设置nr_hugepages的话,mmap会失败:

https://i-blog.csdnimg.cn/img_convert/cce0a3b4ba49eeed17310bfaec60ee4f.png

  1. 标准巨型页是基于hugetlbfs文件系统,需要挂载hugetlbfs文件系统。

https://i-blog.csdnimg.cn/img_convert/1eb791db2925e8a5120857cf69c8a69a.png

  1. sudo ./hugetlb_test,分配一个巨型页(2M)。

https://i-blog.csdnimg.cn/img_convert/c8c0472cdfbd8d7fd35741a8cd7c44b0.png

  1. 测试结果

https://i-blog.csdnimg.cn/img_convert/d2000ac7b762cc5e7d46e34e350d362d.png

https://i-blog.csdnimg.cn/img_convert/0025a68d1b60dcf2c68f810f664a1ba1.png

https://i-blog.csdnimg.cn/img_convert/007bb3fbc7297598a6663a3da3ec1fca.png

sudo cat /proc/$(pidof hugetlb_test)/smaps

https://i-blog.csdnimg.cn/img_convert/952f8ce7276063fda958061531c86ac3.png

匿名映射

// 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

https://i-blog.csdnimg.cn/img_convert/09857d2cd0e7ebf23c902ed95eaf2e5c.png

测试代码

#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;
}

测试结果

https://i-blog.csdnimg.cn/img_convert/6c5d04d54d3eb96e47b01b9e8a5a0daf.png

https://i-blog.csdnimg.cn/img_convert/9f514922f5ec78cf8bdc9a1ac47a9262.png

https://i-blog.csdnimg.cn/img_convert/2bc31329ad27d15f0cdd80004dd6b247.png

标准巨型页(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 的物理连续大页

  • 这是因为:
    1. HugeTLB 必须从 预留池(nr_hugepages) 中分配,这个池子本身就是实际的物理内存。
    2. 内核不能等到缺页时再“凑”一个 2MB 连续块,否则几乎不可能保证可用。
    3. 为了避免“部分映射”问题,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 页节省很多。