服务器入坑指北:大页与页表
内存都用上了、却不知道页表本身吃多少;听人说开大页能提速,但不知道能省多少;同一份读数里数字对不上。
在你动手去读之前,先把三条前提看清:① 同一份/proc/meminfo里两种口径并存——HugePages_*是页数(没有单位后缀),而Hugepagesize/Hugetlb/PageTables是 kB;②HugePages_Total为 0 就是"没有配大页池"——此时谈"往大页里放东西"或去挂载hugetlbfs都没有意义;③ 挂载hugetlbfs需要内核启用 HugeTLB。
这篇给三段可直接粘贴的内容:读两类量 → 算两式 → 改与读回(最后一段配池与改策略是写操作)。取值用占位符,字段名、命令名与参数名照写,并且每个数字都连着它的单位一起写。
页表为什么会长
页表规模与"用了多少内存"成正比、与"页多大"成反比——页越大,项数越少、页表本身越小。
判据:PageTables 那一行给的就是页表自身实占多少内存(kB 口径)——它随常驻内存一起长;想知道"大页能省多少",就要先把项数算出来。
页表规模怎么算
页表项数 ≈ 内存总量 / 页大小——先把两个量换成同一单位再除。这里的"内存总量"用的是 MemTotal(上限口径):它给的是"最坏能有多少项";若要估真实占用,就把常驻量(VmRSS 那一类)代进同一个式子——方法不变,换的是分子。
grep -E -e '^MemTotal:' -e '^PageTables:' /proc/meminfo # 总内存与页表实占:值自带 kB 后缀,kB=1024 字节
cat /proc/meminfo # 兜底:全部字段一次看齐(含 PageTables、Hugetlb)
getconf PAGE_SIZE # 基础页大小(常规页口径,单位字节)
grep -E -e '^Huge' /proc/meminfo # 大页现读:HugePages_* 是页数,Hugepagesize 才是每页多少 kB
算一遍(数值为演示值):MemTotal 取 6291456 kB(6 GiB)⇒ 换成字节 6291456 * 1024 = 6442450944;常规页按 getconf PAGE_SIZE 的 4096 字节 ⇒ 页表项数 ≈ 6442450944 / 4096 = 1572864(项)。
单位不统一会差 1024 倍:把 MemTotal 的 kB 读数当字节直接用,算出来的项数只有实际的 1/1024——所以那一步换算不能省。
从"项数"落到"页表本身吃多少":每项一个页表项、按 8 字节算 ⇒ 1572864 * 8 = 12582912(字节,也就是 12 MiB)⇒ 占这块内存的 12582912 / 6442450944 ≈ 0.195%(约 0.2%)。对照就在你读的第一条命令里:同一次 grep -E -e '^MemTotal:' -e '^PageTables:' /proc/meminfo 里,PageTables 就是页表的实际占用(kB)——把 PageTables 与 MemTotal 相除,就能拿到你机器上真实的那个百分比(我本机现读是 0.42% 量级,与上面按项数估出来的量级可比)。
大页把项数降下来多少
换成大页,项数按"页大小之比"降——大页省的是"页表的那一层",不是"数据的那一层"。
接着算(沿用上面那组 MemTotal,只把页大小换成大页):Hugepagesize 取 2048 kB(也就是 2 MiB)⇒ 换成字节 2048 * 1024 = 2097152;项数 ≈ 6442450944 / 2097152 = 3072(项)。
降了多少:1572864 / 3072 = 512——这就是"项数按页大小之比降"(4 KiB 常规页对 2 MiB 大页,正好是 512 倍)。注意:512 是"页数之间"的倍数关系,不是"页数读数与 kB 读数之间"的换算倍数——两者别混。
判据:它省下来的是"管理开销"——所以小内存机器上收益有限、大内存机器上才明显。
同一份读数里的两种单位
同一份文件里,两种口径相邻排列——这是本题最容易栽的地方:
grep -E -e '^Huge' /proc/meminfo # 页数(无后缀)与 kB(有后缀)就在相邻几行里
sysctl vm.nr_hugepages # 大页池规模:这里同样是页数,不是 kB(版本相关,以现读为准)
sysctl vm.nr_overcommit_hugepages # 池外可临时借的页数(版本相关,以现读为准)
cat /sys/kernel/mm/transparent_hugepage/defrag # 碎片整理策略:决定大页能否真拿到(同样是方括号多值行,方括号里才是当前值)
逐项看后缀:
- 带
kB的是量:MemTotal、PageTables、Hugetlb、Hugepagesize; - 不带后缀的是个数:
HugePages_Total、HugePages_Free、HugePages_Rsvd、HugePages_Surp。
换算时不要用一个写死的倍数:页数与 kB 之间差的是一个Hugepagesize的倍数(以现读为准)——Hugepagesize是多少,倍数就是多少。
页大小的来源也要写清:getconf PAGE_SIZE是命令前端,而 C API 里的名字是sysconf(_SC_PAGESIZE)(getconf _SC_PAGESIZE不是可用写法,会报 “Unrecognized variable”;sysconf的手册页以现读为准)。
取舍:配大页池还是靠透明大页
cat /sys/kernel/mm/transparent_hugepage/enabled # 透明大页模式:多值并列,方括号里那个才是当前生效值
透明大页的那一行读出来长这样(方括号里的是当前生效模式):
always [madvise] never
这个读法要与"读单值"的命令区分开:sysctl vm.nr_hugepages 读的是一个数,而 transparent_hugepage/enabled 是多值并列、靠方括号指明当前值——别把整行当成"当前值"。
三条路各有代价:
- 显式池(
vm.nr_hugepages+hugetlbfs):容量预先划走、行为可预期;代价是划走的内存别的用途就用不了; - 透明大页:按需、无需预留;代价是行为随内核策略变、不容易精确控制(应用侧可以用
MADV_HUGEPAGE按区间申请); - 都不用:省事;代价是大内存下页表开销一直在。
判据:先问"这块内存的访问模式是不是连续大块"。程序要自己指定大页时,接口层是mmap的MAP_HUGETLB(并可配合MAP_HUGE_2MB指定 2 MiB 尺寸)。
改与读回
先看池到底配没配——HugePages_Total 为 0 ⇒ 没配(此时去挂载 hugetlbfs 没有意义):
sysctl vm.nr_hugepages # 改前:记录原值(这里是页数,不是 kB)
sysctl -w vm.nr_hugepages=<页数> # 配池(写操作:需权限,先记原值、留回滚点)
grep -E -e '^Huge' /proc/meminfo # 读回:页数与 Hugepagesize 两个口径分别再看一次
mount -t hugetlbfs none <挂载点> # 挂载大页文件系统(需内核启用 HugeTLB:先在 meminfo 里能看到 HugePages_Total)
池容量与使用率怎么算:大页池容量 = HugePages_Total(页数)× Hugepagesize(kB);已用页数 = HugePages_Total − HugePages_Free(同一份读数里就能拿到,不必依赖 Hugetlb 的语义),已用内存 = 已用页数 × Hugepagesize。
算一遍(这是一组新的演示值):HugePages_Total 取 640(页)× Hugepagesize 2048(kB)⇒ 池容量 = 640 * 2048 = 1310720(kB,1.25 GiB);HugePages_Free 取 400(页)⇒ 已用页数 = 640 - 400 = 240,已用内存 = 240 * 2048 = 491520(kB,480 MiB)⇒ 使用率 = 491520 / 1310720 = 0.375,也就是 37.5%。
Hugetlb 这一项我无法自证:它在有些内核上表示"池占用的内存"、在另一些上表示"已被应用用掉",语义以现读为准——核验路径:在可写 proc/sys 的机器上配一个极小的池(如 sysctl -w vm.nr_hugepages=1)后看 Hugetlb 是"一页的量"还是 0;所以我用上面的 HugePages_Free 式子做判据,Hugetlb 只作旁证。
判据:"没配池"与"配了没用"是两件事——池容量为 0 ⇒ 压根没配;池有容量而 Hugetlb 为 0 ⇒ 配了没人用——先看池容量是不是 0,再谈使用率。配池与改策略是写操作:先记原值、标回滚(sysctl -w 只改运行值,重启即失;要跨重启写进 /etc/sysctl.d/ 再用 sysctl -p 重新读取)。
现象到动作的判定表
| 现象 | 结论 | 动作 |
|---|---|---|
| 页表占用看着偏大 | 先换统一单位再算 | 用 MemTotal ÷ 页大小 估项数,再乘 8 字节换成内存量、与 PageTables 对照 |
| 同一份读数里数字对不上 | 页数对 kB | 逐项看后缀:带 kB 是量、不带是个数 |
| 池容量为 0 | 没配大页池 | 先配池,再谈挂载与使用率 |
| 配了池但没用上 | 已用页数为 0(Total − Free) |
检查应用是否真的走大页路径(MAP_HUGETLB / MADV_HUGEPAGE) |
| 挂载不成功 | 可能内核没启用 HugeTLB | 先在 meminfo 里确认能看到 HugePages_Total |
这篇不解决什么
- 讲zswap 配置取舍的那篇管压缩交换的配置(大页与透明大页在那里只是邻接词);本篇管页大小与页表规模的关系。判定入口:“要不要开压缩交换、怎么配”→ 那篇;“页表吃多少、大页能省多少”→ 本篇。
- 讲内存口径打架时信谁的那篇管多个内存读数不一致时选哪个;本篇用它的结论但只管页表与大页这一路。
- 讲内存超额承诺的取舍的那篇管分配承诺;本篇管已分配内存的页表开销。
踩坑清单
- 把
HugePages_Total(页数)当 kB:同一份文件里两种口径,混用差一个Hugepagesize的倍数。 - 写死一个"512 倍"的换算:倍数取决于
Hugepagesize(512 只是 4 KiB 对 2 MiB 的页数关系)。 - 把
MemTotal当字节:它是 kB,差 1024 倍。 - 池容量为 0 还去挂载:先看
HugePages_Total是不是 0。 - 把
transparent_hugepage/enabled整行当当前值:方括号里那个才是。 - 内核没启用就谈
hugetlbfs:先在 meminfo 里确认。 - 把"省页表"说成"省数据":省的是管理开销那一层。
- 改池不记原值:回滚没有依据。
- 只看项数不看实占:用同一条命令里的
PageTables÷MemTotal,拿到你机器上的真实比值再下结论。
改完怎么验证
- 在机器上对照过:把
PageTables与MemTotal相除,与按项数估出的量级比一比(对不上就回头看单位); - 两个量同单位(
MemTotal的 kB 先换成字节); - 已用页数用
HugePages_Total−HugePages_Free复核过; - 方括号读法写了(
enabled与defrag都是多值行)。
- 点赞
- 收藏
- 关注作者
评论(0)