Hugepages 是 Linux 内核提供的一种内存分配机制,它允许内核以更大的页面大小来分配内存,而不是默认的 4KB 等小页面。这种机制特别适用于那些需要大量连续内存空间的应用程序,比如数据库、虚拟机和某些高性能计算任务。使用 hugepages 的应用程序可以更直接地访问大块内存,而不需要内核进行多次页面映射,提高内存访问效率。
虽然 Linux 提供了 proc 等方式给用户设置大页,但是这些方式往往直接作用于系统而非具体业务本身。glibc 引入的 hugetlb 参数以进程为粒度调整 malloc 的行为,使其根据用户期望的方式从大页申请内存,在提升性能的同时,不影响系统全局的设置。
环境变量取值
hugetlb 环境变量的设置方式与取值如下:
取值 | 含义 | 设置命令 |
0 | 不使用大页 | export GLIBC_TUNABLES=glibc.malloc.hugetlb=0 |
1 | 使用透明大页(THP) | export GLIBC_TUNABLES=glibc.malloc.hugetlb=1 |
2 | 使用系统默认的大页 | export GLIBC_TUNABLES=glibc.malloc.hugetlb=2 |
x(x > 2) | 指定页大小为 x(需与系统支持的页面大小匹配) | export GLIBC_TUNABLES=glibc.malloc.hugetlb=x |
使用透明大页
确认透明大页的启用状态
cat /sys/kernel/mm/transparent_hugepage/enabled
输出示例(方括号表示当前模式):
always [madvise] never
三种模式含义:
模式 | 说明 |
always | 总是使用透明大页 |
madvise | 根据应用程序的建议使用透明大页 |
never | 从不使用透明大页 |
如果设置了
always,则表示整个系统的策略都是优先考虑透明大页,相当于默认支持了此特性,如果可以接受此设置,则本小节可以直接跳过。如果设置了 never,则需要使用如下命令调整:echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
设置 glibc hugetlb 环境变量
export GLIBC_TUNABLES=glibc.malloc.hugetlb=1
测试验证
按照 malloc & memset 测试用例 章节的方法编译并运行测试程序,在赋值完成后查看
/proc/self/smaps 中 AnonHugePages 字段,其值非 0 即表示该进程已使用透明大页,说明透明大页已生效。使用普通大页
当 hugetlb 设置的值大于 1 时,glibc 支持两种使用场景:一种是使用系统默认的大页,另一种是让用户自定义大页,这里分情况介绍。
系统默认大页
确认系统默认大页的大小
实际上,glibc 也是通过读取此字段来获取这个值的:
cat /proc/meminfo | grep Hugepagesize
输出示例:
Hugepagesize: 2048 kB
设置 nr_hugepages
使用普通大页时,需要用户先指定预分配的大页数量。由上一节可知系统默认大页为 2M,因此需要设置
hugepages-2048kB 对应的 nr_hugepages:ls /sys/kernel/mm/hugepages
输出示例:
hugepages-1048576kB hugepages-2048kB
echo 520 > /sys/kernel/mm/hugepages/hugepages-2048kB
设置 glibc hugetlb 环境变量
export GLIBC_TUNABLES=glibc.malloc.hugetlb=2
测试验证
MMUPageSize: 2048 kBPrivate_Hugetlb: 1050624 kB...MMUPageSize: 2048 kBPrivate_Hugetlb: 2048 kB
其中
MMUPageSize(Memory Management Unit Page Size,内存管理单元页大小)为 2048 kB(2M),符合 设置 nr_hugepages 一节中的设置;Private_Hugetlb 有两块,1050624kB 和 2048kB:产生第一个 1050624kB 的原因是
malloc 通过 chunk 管理内存,除了提供 1G 内存之外,还需要预留 chunk 头来管理这块内存,由于此时是 2M 对齐,因此多出了这部分。产生第二个 2048kB 的原因是
malloc 在初次调用时会给 tcache 的数据结构申请空间,又由于 2M 对齐,导致多出了这部分。因此,这里预留了 520 个 2M 的大页(共计 1040M)用于验证 1G 场景
malloc & memset;仅预留 1G 并不够,因为除了 1G 内存本身,还需要为 chunk 头和 tcache 额外预留 2M 对齐的空间。自定义大页
确认系统支持的大页规格
例如,当前的环境支持 1G 和 2M 两种规格,默认是 2M 已经验证过,本小节选择 1G 的大页:
ls /sys/kernel/mm/hugepages
输出示例:
hugepages-1048576kB hugepages-2048kB
设置 nr_hugepages
同上一节,需要预留大页:
echo 3 > /sys/kernel/mm/hugepages/hugepages-1048576kB
这里预留了 3 个 1G 大页用于验证 1G 场景
malloc & memset。设置 glibc hugetlb 环境变量
警告:需要注意,这里的单位是 B(字节),因此需要手动换算。
export GLIBC_TUNABLES=glibc.malloc.hugetlb=1073741824
测试验证
MMUPageSize: 1048576 kBPrivate_Hugetlb: 2097152 kB...MMUPageSize: 1048576 kBPrivate_Hugetlb: 1048576 kB
其中
MMUPageSize(Memory Management Unit Page Size,内存管理单元页大小)为 1048576 kB(1G),符合 设置 nr_hugepages 一节中的设置;Private_Hugetlb 有两块,2097152kB 和 1048576kB:产生第一个 2097152kB 的原因仍是预留 chunk 数据结构加页对齐。
产生第二个 1048576kB 的原因仍是 tcache。
需要解释的是,多出的 1048576kB 不是就不用了,后续如果有小块内存申请,
malloc 还是会优先考虑从这里切内存出去。但是从上例也可以看出大页的弊端,就是使用不当会造成极大的内存浪费,需要开发人员认真规划内存的使用。malloc & memset 测试用例
以下测试程序可用于验证透明大页和普通大页是否生效。程序分配 1GB 内存并进行初始化,随后通过查看进程的
/proc/<PID>/smaps 输出,判断大页使用情况。测试程序
创建测试程序
test_hugepage.c:#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#define MEM_SIZE (1UL << 30) /* 1GB */int main(){char *p = malloc(MEM_SIZE);if (!p) {perror("malloc failed");return 1;}printf("PID: %d\\n", getpid());/* 初始化内存,触发大页分配 */memset(p, 0, MEM_SIZE);printf("已分配并初始化 %lu MB 内存,按 Enter 键退出...\\n", MEM_SIZE >> 20);getchar();free(p);return 0;}
编译并运行
gcc -o test_hugepage test_hugepage.c
./test_hugepage
程序运行后会打印进程 PID 并等待输入,此时在另一个终端执行以下命令查看大页使用情况:
cat /proc/<PID>/smaps | grep -E "AnonHugePages|Private_Hugetlb|MMUPageSize"
验证方法
根据配置的大页类型,查看 smaps 中对应字段是否为非零值:
大页类型 | smaps 字段 | 非零含义 |
透明大页(hugetlb=1) | AnonHugePages | 该进程已使用透明大页 |
普通大页(hugetlb=2 或自定义值) | Private_Hugetlb | 该进程已使用普通大页 |
说明:
对于透明大页,
AnonHugePages 值非 0 表示透明大页已生效;对于普通大页,Private_Hugetlb 值非 0 且 MMUPageSize 与配置的大页规格一致,表示普通大页已生效。原理介绍
本特性涉及的原理如下:
0f982c1827 malloc: Enable huge page support on main arena0849eed45d malloc: Move MORECORE fallback mmap to sysmalloc_mmap_fallbackc1beb51d08 malloc: Add Huge Page support to arenas98d5fcb8d0 malloc: Add Huge Page support for mmap6cc3ccc67e malloc: Move mmap logic to its own function7478c9959a malloc: Add THP/madvise support for sbrk5f6d8d97c6 malloc: Add madvise support for Transparent Huge Pages
变量解析流程
在首次
malloc 时,会调用解析 glibc hugetlb 环境变量的函数,代码如下,value 即为用户指定的值:static __always_inline intdo_set_hugetlb (size_t value){if (value == 1){enum malloc_thp_mode_t thp_mode = __malloc_thp_mode ();/* Only enable THP madvise usage if system does support it andhas 'madvise' mode. Otherwise the madvise() call is wasteful. */if (thp_mode == malloc_thp_mode_madvise)mp_.thp_pagesize = __malloc_default_thp_pagesize ();}else if (value >= 2)__malloc_hugepage_config (value == 2 ? 0 : value,&mp_.hp_pagesize, &mp_.hp_flags);return 0;}
value = 1(透明大页):当
value 为 1 时,通过 __malloc_default_thp_pagesize 函数读取 /sys/kernel/mm/transparent_hugepage/hpage_pmd_size 设置 thp_pagesize 的值:unsigned long int__malloc_default_thp_pagesize (void){int fd = __open64_nocancel ("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", O_RDONLY);if (fd == -1)return 0;char str[INT_BUFSIZE_BOUND (unsigned long int)];ssize_t s = __read_nocancel (fd, str, sizeof (str));__close_nocancel (fd);if (s == 0)return 0;unsigned long int r = 0;for (ssize_t i = 0; i < s; i++){if (str[i] == '\\n')break;r *= 10;r += str[i] - '0';}return r;}
value = 2(系统默认大页):当
value 为 2 时,通过 malloc_default_hugepage_size 获取系统默认的大页大小。value > 2(自定义大页):当
value 大于 2 时,读取 /sys/kernel/mm/hugepage 判断输入的 value 是否为系统支持的大页大小:void__malloc_hugepage_config (size_t requested, size_t *pagesize, int *flags){*pagesize = 0;*flags = 0;if (requested == 0){*pagesize = malloc_default_hugepage_size ();if (*pagesize != 0)*flags = hugepage_flags (*pagesize);return;}/* Each entry represents a supported huge page in the form of:hugepages-<size>kB. */int dirfd = __open64_nocancel ("/sys/kernel/mm/hugepages",O_RDONLY | O_DIRECTORY, 0);if (dirfd == -1)return;char buffer[1024];while (true){...hpsize *= 1024;if (hpsize == requested){*pagesize = hpsize;*flags = hugepage_flags (*pagesize);__close_nocancel (dirfd);return;}}__close_nocancel (dirfd);}
其中
hugepage_flags 实现如下,flags 最终会传递给 mmap,从而指定传统大页的大小:static inline inthugepage_flags (size_t pagesize){return MAP_HUGETLB | (__builtin_ctzll (pagesize) << MAP_HUGE_SHIFT);}
madvise 封装
对于使用透明大页的场景,当
/sys/kernel/mm/transparent_hugepage/enabled 设置为 madvise 时,需要用户态通过 madvise 系统调用告诉内核指定虚拟内存的使用模式,glibc 对 madvise 进行了如下封装:static inline voidmadvise_thp (void *p, INTERNAL_SIZE_T size){# ifdef MADV_HUGEPAGE/* Do not consider areas smaller than a huge page or if the tunableis not active. */if (mp_.thp_pagesize == 0 || size < mp_.thp_pagesize)return;...__madvise (p, size, MADV_HUGEPAGE);# endif}
其中
thp_pagesize 在初次调用 malloc 时初始化为系统默认的透明大页大小。glibc 通过在每次 mmap 后调用 madvise_thp 即可实现使用透明大页。总结
内存大页可以提高内存访问效率,提升性能,但是也会带来内存浪费等问题。glibc 的 hugetlb 环境变量通过更为精细的粒度,在保留大页高效使用内存的同时,在合理规划内存的情况下,可以避免大页过多消耗内存的问题。