巨型页可以增加每条地址翻译缓存能够覆盖的内存范围,在合适负载下减少翻译开销。但效果取决于访问模式和硬件,不能仅因为申请了很大一块内存,就断定程序频繁发生 TLB 缺失。描述工作集需要的页数减少,也不表示所有翻译能够同时装入处理器的 TLB;页数算术、硬件容量与真实性能是三个不同问题。
页大小是一项输入
x86 常见基准页为 4 KiB,但 Linux 的页大小依赖架构与配置,应读取运行系统而不是把四千多字节当作普遍常量。支持的巨型页尺寸同样会变化;2 MiB 与 1 GiB 是常见例子,不是所有处理器的完整列表。跨机器比较时,应先确认单位和实际尺寸,再比较统计数字。
对理想化的连续 8 GiB 范围,4 KiB 粒度需要 2,097,152 个页面,2 MiB 粒度只需 4,096 个。这只是映射覆盖率计算,没有测量硬件 TLB 容量、缺失率、页表遍历延迟,或同时驻留的翻译数量。不同页面大小对应的翻译缓存结构还可能具有不同容量,因此不能把两种页数之差直接称为实际节省的 TLB 项。
显式池与透明分配
Hugetlb 提供显式申请的大页,并由内核维护相应池。持久池中的页面不能同时用于普通分配,也不能在内存压力下换出。池可以在启动时准备,也能在运行中调整,还可能允许额外的 surplus 页面。启动较早时通常更容易获得连续内存,但这并不表示大页只能一次性在开机前决定数量,之后永远无法改变。
透明巨型页采用另一套机制。依内核支持和政策,较大的匿名内存分配既可能发生在缺页时,也可能由后续合并产生。THP 不只是后台线程,也不要求管理员先准备 hugetlb 池。现代内核还可以支持多种 THP 尺寸,不能把每次透明分配都等同于一个两兆字节映射,更不能用 hugetlb 总数代替 THP 使用量。
灵活性也不同。THP 可以退回较小页面,并可为回收等操作拆分。显式 hugetlb 映射不会自动变成普通页映射,但应用在自身配置允许时,可以在失败后另行选择普通页分配。应区分内核映射契约与应用的回退策略,否则会把“必须使用大页”的应用和“优先尝试大页”的应用误写成同一种启动行为。
分别预算容量与位置
在 /proc/meminfo 中,HugePages_Total 表示默认尺寸池的页数,HugePages_Free 是尚未分配的页数,HugePages_Rsvd 是已作出承诺但还没有在缺页时分配的页数。总页数乘页大小得到池容量,不是 Rsvd 所表达的已承诺容量。看起来空闲的池中可能已有一部分被预订,不能把 Free 全部当成任何新申请都能使用的余量。Hugetlb 则统计各种尺寸池占用的总内存,细节应查看分尺寸 sysfs 目录。
稀疏映射可能消耗超过有用数据的常驻内存,池也会与普通内存需求竞争。应按应用文档估算容量,核对实际映射,并给其他分配保留空间。收益需要通过代表性吞吐、延迟及翻译相关测量建立,不能因为配置的大页池足够大就宣布优化已经生效;申请、映射、使用与性能改善是不同阶段。
坏掉的样子
扩大池时,内核可能找不到足够连续的物理内存,因而失败。但已经存在的空闲巨型页本来就是连续页,Free 大于零本身并不是碎片证据。页尺寸不匹配、已有预订、权限、NUMA 位置或 cgroup 限制,也可能解释为什么某个应用用不到看似可用的容量。应按具体失败路径调查,而不是看到内存不足就统一建议重启或重复 mmap。
NUMA 政策可能使页面远离使用它们的 CPU,或者只允许从部分节点分配。应检查分节点分布和访问局部性,再判断是否为地址翻译瓶颈。TLB 改善可以与更差的内存访问延迟同时存在;全局空闲数正常、应用能够启动,都不能证明放置策略合适,也不能推导每次访问一定走本地内存。
THP 分配、合并、整理和拆分会以不同方式影响延迟。thp_collapse_alloc 是合并过程中成功分配的计数,不是停顿时间计数。应将合适的计数增量与延迟共同观察。选择 madvise、always 或当前内核支持的分尺寸政策,需要真实负载依据,不能给所有数据库套用同一个模式,再把模式名称当作性能保证。
快速验证
保存为 hugepage_check.py,运行 python3 hugepage_check.py。程序只读取计数和基准页大小,不创建大页工作负载,也不修改内核设置。缺少数据时明确打印跳过信息,并把假设值标成仅供算术演示。解析与边界断言使用合成字符串,既检查正常输入,也检查单位错误和零页大小,不会把无效数据悄悄解释为真实容量。
import os
import re
from pathlib import Path
WORKING_SET = 8 * 1024**3
FIELDS = ("HugePages_Total", "HugePages_Free", "HugePages_Rsvd",
"HugePages_Surp", "Hugepagesize")
def parse_meminfo(text):
values = {}
for key in FIELDS:
unit = r"\s+kB" if key == "Hugepagesize" else ""
match = re.search(r"^" + key + r":\s+(\d+)" + unit + r"\s*$", text, re.M)
if match:
values[key] = int(match.group(1))
return values
def pages_for(size, page):
if size < 0 or page <= 0:
raise ValueError("invalid byte count or page size")
return (size + page - 1) // page
assert pages_for(4097, 4096) == 2
assert parse_meminfo("HugePages_Total: 8\nHugepagesize: 2048 kB\n")["Hugepagesize"] == 2048
assert parse_meminfo("HugePages_Total: bad\nHugepagesize: 2 MB\n") == {}
try:
pages_for(1, 0)
except ValueError:
pass
else:
raise AssertionError("zero page size accepted")
try:
values = parse_meminfo(Path("/proc/meminfo").read_text())
except OSError as error:
values = {}
print("SKIP live hugetlb counters:", type(error).__name__)
try:
base = os.sysconf("SC_PAGE_SIZE")
assert base > 0
print("Observed base page size:", base, "bytes")
except (AttributeError, ValueError, OSError):
base = 4096
print("Assumed illustration base page size:", base, "bytes")
if all(key in values for key in FIELDS) and values["Hugepagesize"] > 0:
huge = values["Hugepagesize"] * 1024
print("Default-size hugetlb pool counters:", values)
print("Total pool capacity:", values["HugePages_Total"] * huge, "bytes")
print("Reserved but not allocated:", values["HugePages_Rsvd"] * huge, "bytes")
else:
huge = 2 * 1024**2
print("SKIP complete pool report; assuming 2 MiB for arithmetic only")
print("8 GiB page-count illustration:", pages_for(WORKING_SET, base),
"base pages versus", pages_for(WORKING_SET, huge), "large pages")
print("No TLB miss rate, THP coverage, allocation success, or latency was measured")
若基准页为 4 KiB、默认大页为 2 MiB,预期看到前述两组页数。池总数为零是一种有效观察,不证明内核不支持大页。修订脚本已在 Linux、Python 3.12.14 下执行通过。它没有测量 THP 覆盖率、真实分配成功率或性能;这些结论需要另外设计与工作负载对应的实验,不能从一份只读容量报告直接推出。
延伸阅读
来源
- 内核巨型页文档,定义 hugetlb 设置与行为,已核验 2026-06-01。
- 内核透明巨型页文档,定义 THP 模式与后台整理行为,已核验 2026-06-01。
- proc_meminfo:大页计数,核对内存承诺、大页计数与单位,已核验 2026-10-15。