认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》

写在前面


  • 博文内容涉及 Linux 内存中 多级页表,缺页异常,TLB,以及大页相关基本认知
  • 理解不足小伙伴帮忙指正

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》


认识 Linux 内存构成:Linux 内存调优之页表、TLB、大页认知

上一篇博客和小伙伴们分享了内存中虚拟内存和物理内存相关知识,这里我们来看一下 页表,缺页异常,TLB 和大页相关知识

当启动一个程序时,会先给程序分配合适的虚拟地址空间,但是不需要把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存

因为每个进程都维护着自己的虚拟地址空间,每个进程都有一个页表定位虚拟内存到物理内存的映射,每个虚拟内存也在表中都有一个对应的条目

这里的页表是进程用于跟踪虚拟内存到物理内存的映射,那么实际的数据结构是什么的?

页表

如果每个进程都分配一个大的页表,64位系统 理论地址空间为2^64字节,但实际 Linux 系统通常采用48位有效虚拟地址

1
2
3
4
5
6
┌──[root@liruilongs.github.io]-[~]
└─$cat /proc/cpuinfo | grep address
address sizes : 45 bits physical, 48 bits virtual
address sizes : 45 bits physical, 48 bits virtual
┌──[root@liruilongs.github.io]-[~]
└─$

2 ^48字节(256TB)。若页面大小为4KB(2^12字节),则需管理的页表项数量为 虚拟页数 = 2^48 / 2^12 = 2^36

每个页表项需要存储物理页帧号(PFN)和权限标志,通常占用8字节。所以页表的总内存需求为: 总大小 = 2^36 × 8 = 2^39 字节 = 512GB

512G ,即一个进程的页表本身就是巨大的,如果多个进程更夸张,但是实际中进程仅使用少量内存(如1GB),可能只需要几个映射,单级页表仍需预分配全部虚拟地址空间对应的页表项,造成大部分的空间浪费,况且也没有那么多内存存放页表。

多级页表

这里优化的方案就是将页面分级管理(多级页表 Multi-Level Page Table),将一个大页表大小分成很多小表,最终指向页表条目(一条映射记录),系统只需要给进程分配页表目录,从而降低映射总表的大小。

这里怎么理解多级页表和页表目录?

想象你要管理一个超大的图书馆(相当于虚拟地址空间),里面有 ​​几百万本书​​(相当于内存页)。如果用一个超大的总目录(只有小标题)记录每本书的位置,这个目录本身就会占据整个图书馆的空间,显然不现实。多级页表就像是对只有小标题的目录作了多级目录划分。

  • 第一层目录(PGD)​​:记录整个图书馆分为 ​​512个大区​​(每个大区对应9位索引,2⁹=512)。
  • ​​第二层目录(PUD)​​:每个大区再分为 ​​512个小区​​。
  • ​​第三层目录(PMD)​​:每个小区再分 ​​512个书架​​。
  • ​​第四层目录(PTE)​​:每个书架对应 ​​512本书​​(每本书即4KB内存页)

我们知道数组存储只存储首地址,之后的元素会根据首地址计算,这里的页表目录类似首地址,所以可以通过多级目录位置直接定位映射记录。

现代系统多使用上面多级页表(如 x86-64 的 ​​四级页表​​)的方式,逐步缩小搜索范围,但是多级页表也有一定的弊端,后面我们会讨论

首先会按照上面的方式对 48位虚拟地址进行拆分​​,虚拟地址被分割为多个索引字段,每一级索引对应一级页表,逐级查询页表项(PTE),48 位虚拟地址可能拆分为:

PGD索引(9位) → PUD索引(9位) → PMD索引(9位) → PTE索引(9位) → 页内偏移(12位)

每级页表仅需512(2^9)项(9位索引),每个表项是 8 字节,所以单级占用4KB,而且仅在实际需要时分配下级页表。

当进程需要映射1GB内存时,只需要分配必要的页表仅需

总页表大小 = 1(PGD)+1(PUD)+1(PMD)+512(PTE) = 515×4KB ≈ 2.02MB

PGD、PUD、PMD各一个,PTE需要512个,总共515个页表项,每个4KB,总共约2.02MB

那么这里的 512个索引页面是如何计算的?

1GB/4KB=262,144个页面, 262,144/512=512PTE索引页(一个索引页存放512页表项),一个页表项对应一个内存页

前面我们也有讲过,在具体的分配上,内核空间位于虚拟地址的高位(高24位),用户态内存空间位于虚拟地址低位,页表本身存储在内核空间,用户程序无法直接修改,仅能通过系统调用请求内核操作。用户态程序申请内存时,内核仅分配虚拟地址,实际物理页的分配由缺页异常触发。此时内核介入,更新页表项映射物理页,这一过程需切换到内核态执行。

那里这里的缺页异常又是什么?

缺页异常

当进程访问系统没有映射物理页的虚拟内存页时,内核就会产生一个 page fault 异常事件。

minor fualt

当进程缺页事件发生在第一次访问虚拟内存时,虚拟内存已分配但未映射(如首次访问、写时复制、共享内存同步)物理地址,内核会产生一个 minor page fualt,并分配新的物理内存页。minor page fault 产生的开销比较小。

minor page fualt 典型场景​:

  • 首次访问​​:进程申请内存后,内核延迟分配物理页(Demand Paging),首次访问时触发。
  • 写时复制(COW)​​:fork()创建子进程时共享父进程内存,子进程写操作前触发​​
  • 共享库加载​​动态链接库被多个进程共享,首次加载到物理内存时触发,即会共享页表

major fault

当物理页未分配且需从磁盘(Swap分区或文件)加载数据,内核就会产生一个 majorpage fault,比如内核通过Swap分区,将内存中的数据交换出去放到了硬盘,需要时从硬盘中重新加载程序或库文件的代码到内存。涉及到磁盘I/O,因此一个major fault对性能影响比较大,​​典型场景​有

  • ​​Swap In​​:物理内存不足时,内核将内存页换出到 Swap 分区,再次访问需换回。
  • ​​文件映射(mmap)​​:通过 mmap 映射文件到内存,首次访问文件内容需从磁盘读取。

​​Minor Fault​​ 是内存层面的轻量级操作,​​Major Fault​​ 是涉及磁盘I/O的重型操作。频繁的 ​​Major Fault​​ 就需要考虑性能问题, 对于缺页异常,我们通过 ps、vmstat、perf等工具定位性能瓶颈

通过 ps 命令查看当前系统存在缺页异常的进程的排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌──[root@liruilongs.github.io]-[~]
└─$ps -eo pid,minflt,majflt,comm | awk '$2 > 0 && $3 > 0 {print}'
PID MINFLT MAJFLT COMMAND
1 55646 189 systemd
704 959 7 systemd-journal
719 1912 2 systemd-udevd
892 80 3 auditd
913 553 12 dbus-broker-lau
915 281 4 dbus-broker
918 15617 206 firewalld
919 325 6 irqbalance
921 740 5 systemd-logind
925 166 5 chronyd
955 1243 100 NetworkManager
991 26090 281 /usr/sbin/httpd
998 2683 260 php-fpm
999 923 17 sshd
1002 9775 7 tuned
1006 862 3 crond
1121 6976 225 mariadbd
1150 2060 125 polkitd
1213 731 24 rsyslogd
1498 390 7 pmcd
1518 516 11 pmdaroot
1535 470 4 pmdaproc
1544 410 2 pmdaxfs
1551 447 4 pmdalinux
1558 409 2 pmdakvm
1872 2109 1 /usr/sbin/httpd
1874 3701 9 /usr/sbin/httpd
2201 2654 2 bash
2245 678 6 sudo
2246 3300 1 bash
4085 541 10 htop
┌──[root@liruilongs.github.io]-[~]
└─$

也可以通过 perf stat 来查看指定命令,进程的 缺页异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌──[root@liruilongs.github.io]-[~]
└─$perf stat -e minor-faults,major-faults hostnamectl
Static hostname: liruilongs.github.io
Icon name: computer-vm
Chassis: vm �
Machine ID: 7deac2815b304f9795f9e0a8b0ae7765
Boot ID: 5041b68a4d574df2b59289e33e85bdd5
Virtualization: vmware
Operating System: Rocky Linux 9.4 (Blue Onyx)
CPE OS Name: cpe:/o:rocky:rocky:9::baseos
Kernel: Linux 5.14.0-427.20.1.el9_4.x86_64
Architecture: x86-64
Hardware Vendor: VMware, Inc.
Hardware Model: VMware Virtual Platform
Firmware Version: 6.00

Performance counter stats for 'hostnamectl':

463 minor-faults
0 major-faults

0.132397887 seconds time elapsed

0.009642000 seconds user
0.004471000 seconds sys


┌──[root@liruilongs.github.io]-[~]
└─$

可以看到 hostnamectl 命令因内存动态分配触发了 463 次次缺页中断,下面是一些常见的对应缺页异常的调优建议

​​减少 Major Fault​​:

  • 增加或者禁用物理内存​​:避免频繁 Swap。
  • ​​调整 Swappiness​​:降低内核参数 /proc/sys/vm/swappiness,减少内存换出倾向。
  • ​预加载数据​​:使用 mlock() 锁定关键内存页(如实时系统),禁止换出。
  • 优化文件访问​​:对 mmap 文件进行顺序读取或预读(posix_fadvise)。
    ​​

降低 Minor Fault​​:

  • ​​预分配内存​​:避免 Demand Paging 的延迟(如启动时初始化全部内存)。
  • ​减少 COW 开销​​:避免频繁 fork(),改用 posix_spawn 或线程。

通过多级页表的方式极大的缩小和页表空间,可以按需分配,但是多级页表也有一定的局限性​​,一是地址转换复杂度​​,层级增加会降低转换效率,需依赖硬件加速(如MMU的并行查询能力)。二是内存碎片风险​​,子表的离散分配可能导致物理内存碎片化(内存不连续+频繁的回收创建),需操作系统优化分配策略

为了解决多级页表的地址转换需多次访存(如四级页表需4次内存访问),导致延迟增加,常见的解决方案包括:

  • TLB(快表)缓存​​:存储最近使用的页表项,命中时直接获取物理地址,减少访存次数​
  • ​巨型页(Huge Page)​​:使用2MB或1GB的页面粒度,减少页表层级和项数(大页的使用需要操作系统和应用程序的支持)

所以进程通过页表查询虚拟地址和物理地址的映射关系, 首先会检查 TLB(Translation Lookaside Buffer)高速缓存页表项CPU硬件缓存.

那么这里的 TLB 是如何参与到到内存映射的?

TLB

TLB 是内存管理单元(MMU)的一部分,本质是​​页表的高速缓存​​,存储最近被频繁访问的页表项(虚拟地址到物理地址的映射关系)的副本,是集成在 CPU 内部的 ​​高速缓存硬件​​,用于加速虚拟地址到物理地址转换的专用缓存,通过专用电路实现高速地址转换,与数据缓存(Data Cache)指令缓存(Instruction Cache)并列,共同构成 CPU 缓存体系

上面我们讲当进程查询分层页面的映射信息会导致延迟增加。因此,当缺页异常触发内核分配物理内存将从虚拟地址到物理地址的映射添加到页表中时,它还将该映射条目缓存在 TLB 硬件缓存中,通过缓存进程最近使用的页映射来加速地址转换。

当下一次查询发生的时候,首先会在 TLB 中查询是否有缓存,如果有的话会直接获取,没有的话,走上面缺页异常的流程

1
2
3
4
5
进程访问虚拟地址 → MMU 查询 TLB → [命中 → 直接获取物理地址]

└→ [未命中 → 查询页表 → 权限检查 → 缺页处理(可选)→ 生成物理地址 → 更新 TLB]

└→ 访问物理内存

所以 TLB 命中率直接影响程序效率。若 TLB 未命中(Miss),需通过页表遍历获取物理地址,导致额外延迟(通常是 TLB 命中时间的数十倍),从内存加载页表项,并更新TLB缓存(可能触发条目替换,如LRU算法)

可以通过 perf stat 命令来查看某一个命令或者进程的 TLB 命中情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌──[root@liruilongs.github.io]-[~]
└─$perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses hostnamectl
Static hostname: liruilongs.github.io
Icon name: computer-vm
Chassis: vm �
Machine ID: 7deac2815b304f9795f9e0a8b0ae7765
Boot ID: 5041b68a4d574df2b59289e33e85bdd5
Virtualization: vmware
Operating System: Rocky Linux 9.4 (Blue Onyx)
CPE OS Name: cpe:/o:rocky:rocky:9::baseos
Kernel: Linux 5.14.0-427.20.1.el9_4.x86_64
Architecture: x86-64
Hardware Vendor: VMware, Inc.
Hardware Model: VMware Virtual Platform
Firmware Version: 6.00

Performance counter stats for 'hostnamectl':

0 dTLB-loads
0 dTLB-load-misses
<not supported> iTLB-loads
0 iTLB-load-misses

0.131288737 seconds time elapsed

0.010681000 seconds user
0.005377000 seconds sys


┌──[root@liruilongs.github.io]-[~]
  • dTLB-loads 表示数据地址转换(Data TLB)的加载次数,
  • dTLB-load-misses 表示未命中次数
  • iTLB-loads 表示指令地址转换(Instruction TLB)的加载次数
  • iTLB-load-misses 指令 TLB 未命中次数为 0,说明所有指令访问均命中 TLB 缓存。

查看指定进程的命中情况使用 -p <pid> 的方式

1
2
3
4
5
┌──[root@liruilongs.github.io]-[/usr/lib/systemd/system] 
└─$perf stat -e dTLB-loads,dTLB-load-misses,iTLB-loads,iTLB-load-misses -p 1
┌──[root@liruilongs.github.io]-[/usr/lib/systemd/system]
└─$

大页(巨型页)

大页是另一种解决多级页表多次访问内存的手段,顾名思义,传统的内存页是 4KB,大于 4KB 的内存页被称为大页,通过大页可以降低多级页表的层级

同时 TLB 也有一定的局限性,存储条目是固定的,当进程需要访问大量内存的时候,比如数据库应用,将会导致大量 TLB 未命中而影响性能,还是需要通过多级页表来转化地址,所以除了 4KB 页面之外,Linux 内核还通过大页面机制支持大容量内存页面

通过查看 /proc/meminfo 文件确定具体系统的大页大小以及使用情况,大页分为 标准大页(静态大页)透明大页

静态大页其核心原理是通过增大内存页的尺寸(如2MB或1GB),优化虚拟地址到物理地址的转换效率,从而提升系统性能。x86 64 位架构支持多种大页规格,比如 4KiB,2MiB 以及 1GiB。Linux 系统默认是 2MiB

需要说明的是,大页配置仅受用语支持大页的应用程序,对于不支持大页的应用程序来说是无效的,同时大页会导致内存剩余空间变小 后面我们会介绍几个Demo

透明大页用于合并传统内存页

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──[root@liruilongs.github.io]-[~]
└─$cat /proc/meminfo | grep Hug
AnonHugePages: 165888 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
┌──[root@liruilongs.github.io]-[~]
└─$

Hugepagesize: 2048 kB​​: 静态大页的默认大小为 ​​2MB​​,这里的大页是标准大页,若需使用 1GB 大页,需修改内核参数配置,前提是需要CPU 支持才行

AnonHugePages: 165888 kB: ​​透明大页(Transparent HugePages)​​ 匿名页占用的内存总量为 ​​165,888 KB(约 162 MB)

大部分部署数据库的机器会禁用透明大页?这是什么原因

透明大页

透明大页(Transparent Huge Pages,THP)是内核提供的一种动态内存管理机制,它通过自动将多个 ​​4KB 小页​​ 合并为 ​​2MB 或 1GB 大页​​,减少页表项数量并提升 ​​TLB(地址转换缓存)命中率​​,从而优化内存访问性能

与需手动预分配的静态大页(HugeTLB)不同,THP 对应用程序透明且无需配置,适用于​​顺序内存访问​​(如大数据处理)和低实时性场景。但动态合并可能引发 ​​内存碎片​​ 和 ​​性能抖动​​,因此对延迟敏感的数据库(如 MySQL)或高并发系统建议关闭 THP

下面为透明大页相关配置

1
2
3
┌──[root@liruilongs.github.io]-[~] 
└─$ls /sys/kernel/mm/transparent_hugepage/
defrag enabled hpage_pmd_size khugepaged/ shmem_enabled use_zero_page

enabled 用于配置是否开启 THP

1
2
3
4
5
6
7
8
9
┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
┌──[root@liruilongs.github.io]-[~]
└─$cat /proc/meminfo | grep Anon
AnonPages: 356692 kB
AnonHugePages: 167936 kB
┌──[root@liruilongs.github.io]-[~]
└─$

禁用透明大页 某些场景(如数据库)建议禁用 THP 以稳定性能:

1
2
# 临时禁用
echo never > /sys/kernel/mm/transparent_hugepage/enabled

使用 grubby 更新内核启动参数​​,grubby 用于 ​​动态修改内核启动参数​​ 或 ​​设置默认内核​​,无需手动编辑配置文件

1
2
3
4
5
# 永久禁用
┌──[root@liruilongs.github.io]-[~]
└─$grubby --update-kernel=ALL --args="transparent_hugepage=never"
┌──[root@liruilongs.github.io]-[~]
└─$reboot

确认配置

1
2
3
4
5
┌──[root@liruilongs.github.io]-[~] 
└─$cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
┌──[root@liruilongs.github.io]-[~]
└─$

再次查看透明大页使用情况

1
2
3
4
5
┌──[root@liruilongs.github.io]-[~] 
└─$grep AnonHugePages /proc/meminfo
AnonHugePages: 0 kB
┌──[root@liruilongs.github.io]-[~]
└─$

shmem_enabled 用于配置 ​​共享内存(如 tmpfs、共享匿名映射)是否启用透明大页(THP)

1
2
3
┌──[root@liruilongs.github.io]-[~] 
└─$cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
always within_size advise [never] deny force

这里的 never 表示完全禁用共享内存的透明大页​​。常用于数据库(如 Oracle、MySQL)或高延迟敏感型应用,避免因动态内存合并引发性能抖动

透明大页会涉及到一个进程 khugepagedkhugepaged 是 Linux 内核的一部分,负责处理透明大页(Transparent HugePages, THP)的管理。透明大页是内核自动将小页合并为大页以提升性能的机制,而 khugepaged 就是负责这个合并过程的守护进程。​​自动扫描内存区域​​,寻找可以合并的小页,并尝试将它们转换为透明大页。此过程在后台静默运行,无需应用程序显式请求。

1
2
3
4
┌──[root@liruilongs.github.io]-[/sys/devices/system/node] 
└─$ps -eaf | grep khug
root 44 2 0 4月23 ? 00:00:00 [khugepaged]
root 16049 9747 0 10:49 pts/0 00:00:00 grep --color=auto khug

它会尝试将多个常规小页(4KB)合并成 ​​大页(2MB 或 1GB)​​,以减少页表项数量,从而提升内存访问性能。

控制 khugepaged扫描频率,合并阈值等可以通过下面的文件修改

1
2
3
4
5
6
7
8
┌──[root@liruilongs.github.io]-[/sys/devices/system/node] 
└─$cat /sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
60000
┌──[root@liruilongs.github.io]-[/sys/devices/system/node]
└─$cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
10000
┌──[root@liruilongs.github.io]-[/sys/devices/system/node]
└─$

静态大页

静态大页需要单独配置,使用 sysctl 修改内核参数,可以设置分配的静态大页的数量,大页内存是系统启动时或通过 sysctl ​​预先分配​​的,这部分内存会被锁定,普通进程无法使用,所以配置需要考虑清楚

1
2
3
4
5
6
7
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -a | grep huge
vm.hugetlb_optimize_vmemmap = 0
vm.hugetlb_shm_group = 0
vm.nr_hugepages = 0
vm.nr_hugepages_mempolicy = 0
vm.nr_overcommit_hugepages = 0

vm.nr_hugepages​​:表示系统要预留的 ​​大页数量,通过 -w 临时配置内核参数,配置大页数量为 50

1
2
3
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -w vm.nr_hugepages=50
vm.nr_hugepages = 50

确认配置

1
2
3
4
5
6
7
8
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -a | grep huge
vm.hugetlb_optimize_vmemmap = 0
vm.hugetlb_shm_group = 0
vm.nr_hugepages = 50
vm.nr_hugepages_mempolicy = 50
vm.nr_overcommit_hugepages = 0

永久生效将配置写入 /etc/sysctl.conf 并执行 sysctl -p

可以通过 grub 修改内核参数来设置大页的数量以及大小

/etc/default/grub 是 Linux 系统中用于配置 ​​GRUB(GRand Unified Bootloader)​​ 引导程序的核心文件。GRUB 是大多数 Linux 发行版默认的启动管理器,负责在系统启动时加载内核和初始化内存盘(initramfs)。该文件定义了 GRUB 的全局行为和启动菜单的默认选项。和 上面 grubby 的方式略有区别

  • hugepages=N : 设置大页的数量
  • hugepagesz=N 或 default_hugepagesz=N 设置大页大小(默认 2MiB)

下面是一个Demo

1
2
3
4
5
6
7
8
9
10
11
12
┌──[root@liruilongs.github.io]-[~]
└─$cat /etc/default/grub
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M resume=/dev/mapper/rl-swap rd.lvm.lv=rl/root rd.lvm.lv=rl/swap"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true
┌──[root@liruilongs.github.io]-[~]
└─$

GRUB_CMDLINE_LINUX 传递给所有 Linux 内核的公共启动参数(包括默认内核和恢复模式内核)

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──[root@liruilongs.github.io]-[~]
└─$vim /etc/default/grub
8L, 374B written
┌──[root@liruilongs.github.io]-[~]
└─$cat /etc/default/grub
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="default_hugepagesz=1G hugepages=10 hugepagesz=1G net.ifnames=0 consoleblank=600 console=tty0 console=ttyS0,115200n8 spectre_v2=off nopti noibrs noibpb selinux=0 crashkern
el=512M"
GRUB_DISABLE_RECOVERY="true"

上面的配置 hugepages=10 hugepagesz=1G,静态大页大小为 1G,数量为 10

需要说明的是,大页需要使用连续的内存空间,尽量设置永久规则,在开机时分配大页,如果系统已经运行了很久,大量的内存碎片,有可能无法分配大页,因为没有足够的连续内存空间

配置 1G 的静态大页需要CPU 支持,检查是否包含 pdpe1gb 标签

1
2
3
4
5
6
7
8
9
┌──[root@liruilongs.github.io]-[~] 
└─$grep pdpe1gb /proc/cpuinfo
flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology no
nstop_tsc cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_s
ingle ssbd ibrs ibpb stibp ibrs_enhanced fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec
xgetbv1 arat avx512_vnni md_clear flush_l1d arch_capabilities
.........................................
┌──[root@liruilongs.github.io]-[~]
└─$

修改之后使用 grub2-mkconfig 生成了新的 GRUB 配置文件。重启系统使配置生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──[root@liruilongs.github.io]-[~] 
└─$grub2-mkconfig -o /boot/grub2/grub.cfg
正在生成 grub 配置文件 ...
找到 Linux 镜像:/boot/vmlinuz-5.10.0-60.139.0.166.oe2203.x86_64
找到 initrd 镜像:/boot/initramfs-5.10.0-60.139.0.166.oe2203.x86_64.img
找到 Linux 镜像:/boot/vmlinuz-5.10.0-60.18.0.50.oe2203.x86_64
找到 initrd 镜像:/boot/initramfs-5.10.0-60.18.0.50.oe2203.x86_64.img
找到 Linux 镜像:/boot/vmlinuz-0-rescue-f902bd6553f24605a695d4a876a40b7a
找到 initrd 镜像:/boot/initramfs-0-rescue-f902bd6553f24605a695d4a876a40b7a.img
Adding boot menu entry for UEFI Firmware Settings ...
完成
┌──[root@liruilongs.github.io]-[~]
└─$reboot

确认配置,可以看到 Hugepagesize 是1G,但是 nr_hugepages 大小为 5 ,并不是我们配置的 10,这是什么原因,前面我们讲,静态大页会直接分配内存,即只有配置就会位于常驻内存,当系统内存没有配置的静态大页大时,系统会自动减少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──[root@liruilongs.github.io]-[~] 
└─$cat /proc/meminfo | grep Hug
AnonHugePages: 131072 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 5
HugePages_Free: 5
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 1048576 kB
Hugetlb: 5242880 kB
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -a | grep huge
vm.hugepage_mig_noalloc = 0
vm.hugepage_nocache_copy = 0
vm.hugepage_pmem_allocall = 0
vm.hugetlb_shm_group = 0
vm.nr_hugepages = 5
vm.nr_hugepages_mempolicy = 5
vm.nr_overcommit_hugepages = 0
┌──[root@liruilongs.github.io]-[~]
└─$

我们可以使用 free 命令查看内存使用情况验证这一点

1
2
3
4
5
┌──[root@liruilongs.github.io]-[~] 
└─$free -g
total used free shared buff/cache available
Mem: 7 5 0 0 0 1
Swap: 0 0 0

通过临时修改内核参数调整静态大页数目(实际调整需要考虑静态大页是否使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──[root@liruilongs.github.io]-[~] 
└─$sysctl -w vm.nr_hugepages=2
vm.nr_hugepages = 2
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -a | grep nr_hug
vm.nr_hugepages = 2
vm.nr_hugepages_mempolicy = 2
┌──[root@liruilongs.github.io]-[~]
└─$cat /proc/meminfo | grep Hug
AnonHugePages: 169984 kB
ShmemHugePages: 0 kB
FileHugePages: 0 kB
HugePages_Total: 2
HugePages_Free: 2
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 1048576 kB
Hugetlb: 2097152 kB

确认配置是否生效

1
2
3
4
5
6
7
┌──[root@liruilongs.github.io]-[~] 
└─$free -g
total used free shared buff/cache available
Mem: 7 2 3 0 0 4
Swap: 0 0 0
┌──[root@liruilongs.github.io]-[~]
└─$

为了让进程可以使用大页,进程必须进行系统函数调用,可以调用 mmap() 函数,或者 shmat() 函数,又或者是 shmget()函数。如果进程使用的是 mmap()系统函数调用,则必须挂载-个 hugetlbfs 文件系统。

1
2
3
4
┌──[root@liruilongs.github.io]-[~]
└─$mkdir /largepage
┌──[root@liruilongs.github.io]-[~]
└─$mount -t hugetlbfs none /largepage

如果在 NUMA 系统上,内核将大页划分到所有 NUMA 节点上,对应的静态大页参数需要分别设置,而不用设置全局参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
50
┌──[root@liruilongs.github.io]-[~]
└─$echo 20 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/devices/system/node/node0/meminfo
Node 0 MemTotal: 16082492 kB
Node 0 MemFree: 14969744 kB
Node 0 MemUsed: 1112748 kB
Node 0 SwapCached: 0 kB
Node 0 Active: 562104 kB
Node 0 Inactive: 215520 kB
Node 0 Active(anon): 369388 kB
Node 0 Inactive(anon): 0 kB
Node 0 Active(file): 192716 kB
Node 0 Inactive(file): 215520 kB
Node 0 Unevictable: 0 kB
Node 0 Mlocked: 0 kB
Node 0 Dirty: 28140 kB
Node 0 Writeback: 0 kB
Node 0 FilePages: 420444 kB
Node 0 Mapped: 93924 kB
Node 0 AnonPages: 356560 kB
Node 0 Shmem: 12208 kB
Node 0 KernelStack: 9744 kB
Node 0 PageTables: 6216 kB
Node 0 SecPageTables: 0 kB
Node 0 NFS_Unstable: 0 kB
Node 0 Bounce: 0 kB
Node 0 WritebackTmp: 0 kB
Node 0 KReclaimable: 45436 kB
Node 0 Slab: 111576 kB
Node 0 SReclaimable: 45436 kB
Node 0 SUnreclaim: 66140 kB
Node 0 AnonHugePages: 167936 kB
Node 0 ShmemHugePages: 0 kB
Node 0 ShmemPmdMapped: 0 kB
Node 0 FileHugePages: 0 kB
Node 0 FilePmdMapped: 0 kB
Node 0 Unaccepted: 0 kB
Node 0 HugePages_Total: 20
Node 0 HugePages_Free: 20
Node 0 HugePages_Surp: 0
┌──[root@liruilongs.github.io]-[~]
└─$

透明大页 vs 静态大页简单比较

特性 透明大页(THP) 静态大页(Huge Pages)
配置方式 内核自动管理,无需用户干预。 需手动预留(如通过 /etc/default/grub)。
适用场景 通用型应用(如 Java、Web 服务)。 高性能计算、数据库(如 Oracle、MySQL)。
内存碎片化 可能因频繁合并/拆分导致碎片。 预留固定内存,无碎片问题。
性能稳定性 可能因动态合并产生性能波动。 性能更稳定可控。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知:)


《 Red Hat Performance Tuning 442 》

《性能之巅 系统、企业与云可观测性(第2版)》


© 2018-至今 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知

https://liruilongs.github.io/2025/04/18/待发布/认识 Linux 内存构成:Linux 内存调优之页表、TLB、大页认知/

发布于

2025-04-18

更新于

2025-04-25

许可协议

评论
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×