虽然 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
十一、测试验证
按照 malloc & memset 测试用例 章节的方法编译并运行测试程序,在赋值之后查看 /proc/self/smaps 中 Private_Hugetlb 非 0 的字段,得到如下日志:
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
十六、测试验证
按照 malloc & memset 测试用例 章节的方法编译并运行测试程序,在赋值之后查看 /proc/self/smaps 中 Private_Hugetlb 非 0 的字段,得到如下日志:
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"
20、验证方法
根据配置的大页类型,查看 smaps 中对应字段是否为非零值:
| 大页类型 | smaps 字段 | 非零含义 |
| 透明大页(hugetlb=1) | AnonHugePages | 该进程已使用透明大页 |
| 普通大页(hugetlb=2 或自定义值) | Private_Hugetlb | 该进程已使用普通大页 |
注意:说明:
注意:对于透明大页,AnonHugePages 值非 0 表示透明大页已生效;对于普通大页,Private_Hugetlb 值非 0 且 MMUPageSize 与配置的大页规格一致,表示普通大页已生效。
21、原理介绍
本特性涉及的原理如下:
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
22、变量解析流程
在首次 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 and has '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);}
23、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 tunable is 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 即可实现使用透明大页。
24、总结
内存大页可以提高内存访问效率,提升性能,但是也会带来内存浪费等问题。glibc 的 hugetlb 环境变量通过更为精细的粒度,在保留大页高效使用内存的同时,在合理规划内存的情况下,可以避免大页过多消耗内存的问题。