admin管理员组文章数量:1028438
认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知
写在前面
- 博文内容涉及 Linux 内存中
多级页表,缺页异常,TLB,以及大页
相关基本认知 - 理解不足小伙伴帮忙指正
对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
认识 Linux 内存构成:Linux 内存调优之页表、TLB、大页认知
上一篇博客和小伙伴们分享了内存中虚拟内存和物理内存相关知识,这里我们来看一下 页表,缺页异常,TLB 和大页相关知识
当启动一个程序时,会先给程序分配合适的虚拟地址空间
,但是不需要把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存
因为每个进程都维护着自己的虚拟地址空间,每个进程都有一个页表
来定位虚拟内存到物理内存的映射
,每个虚拟内存也在表中都有一个对应的条目
。
这里的页表是进程用于跟踪虚拟内存到物理内存的映射,那么实际的数据结构是什么的?
页表
如果每个进程都分配一个大的页表,64位系统 理论虚拟地址空间为2^64
字节,但实际 Linux 系统通常采用48
位有效虚拟地址
┌──[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=512
个PTE索引页(一个索引页存放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
命令查看当前系统存在缺页异常的进程
的排序
┌──[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
来查看指定命令,进程的 缺页异常情况
┌──[root@liruilongs.github.io]-[~]
└─$perfstat -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
中查询是否有缓存,如果有的话会直接获取,没有的话,走上面缺页异常的流程
进程访问虚拟地址 → MMU 查询 TLB → [命中 → 直接获取物理地址]
│
└→ [未命中 → 查询页表 → 权限检查 → 缺页处理(可选)→ 生成物理地址 → 更新 TLB]
│
└→ 访问物理内存
所以 TLB
命中率直接影响程序效率。若 TLB 未命中(Miss)
,需通过页表遍历获取物理地址,导致额外延迟(通常是 TLB 命中时间的数十倍)
,从内存加载页表项,并更新TLB缓存(可能触发条目替换,如LRU算法)
可以通过 perf stat
命令来查看某一个命令或者进程的 TLB
命中情况
┌──[root@liruilongs.github.io]-[~]
└─$perfstat -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>
的方式
┌──[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
透明大页用于合并传统内存页
代码语言:javascript代码运行次数:0运行复制┌──[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
下面为透明大页相关配置
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$ls /sys/kernel/mm/transparent_hugepage/
defrag enabled hpage_pmd_size khugepaged/ shmem_enabled use_zero_page
enabled 用于配置是否开启 THP
代码语言:javascript代码运行次数:0运行复制┌──[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 以稳定性能:
代码语言:javascript代码运行次数:0运行复制# 临时禁用
echo never > /sys/kernel/mm/transparent_hugepage/enabled
使用 grubby 更新内核启动参数,grubby 用于 动态修改内核启动参数 或 设置默认内核,无需手动编辑配置文件
代码语言:javascript代码运行次数:0运行复制# 永久禁用
┌──[root@liruilongs.github.io]-[~]
└─$grubby --update-kernel=ALL --args="transparent_hugepage=never"
┌──[root@liruilongs.github.io]-[~]
└─$reboot
确认配置
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
┌──[root@liruilongs.github.io]-[~]
└─$
再次查看透明大页使用情况
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$grep AnonHugePages /proc/meminfo
AnonHugePages: 0 kB
┌──[root@liruilongs.github.io]-[~]
└─$
shmem_enabled 用于配置 共享内存(如 tmpfs、共享匿名映射)是否启用透明大页(THP)
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
always within_size advise [never] deny force
这里的 never
表示完全禁用共享内存的透明大页。常用于数据库(如 Oracle、MySQL)或高延迟敏感型应用,避免因动态内存合并引发性能抖动
透明大页会涉及到一个进程 khugepaged
,khugepaged
是 Linux 内核的一部分,负责处理透明大页(Transparent HugePages, THP)的管理
。透明大页是内核自动将小页合并为大页以提升性能的机制,而 khugepaged 就是负责这个合并过程的守护进程。自动扫描内存区域,寻找可以合并的小页,并尝试将它们转换为透明大页。此过程在后台静默运行,无需应用程序显式请求。
┌──[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
的扫描频率,合并阈值
等可以通过下面的文件修改
┌──[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 预先分配的,这部分内存会被锁定,普通进程无法使用
,所以配置需要考虑清楚
┌──[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
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -w vm.nr_hugepages=50
vm.nr_hugepages = 50
确认配置
代码语言:javascript代码运行次数:0运行复制┌──[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
代码语言:javascript代码运行次数:0运行复制┌──[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
内核的公共启动参数(包括默认内核和恢复模式内核)
┌──[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
标签
┌──[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 配置文件。重启系统使配置生效。
┌──[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
,这是什么原因,前面我们讲,静态大页会直接分配内存,即只有配置就会位于常驻内存,当系统内存没有配置的静态大页大时,系统会自动减少
┌──[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 命令查看内存使用情况验证这一点
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$free -g
total used free shared buff/cache available
Mem: 7 5 0 0 0 1
Swap: 0 0 0
通过临时修改内核参数调整静态大页数目(实际调整需要考虑静态大页是否使用)
代码语言:javascript代码运行次数:0运行复制┌──[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
确认配置是否生效
代码语言:javascript代码运行次数:0运行复制┌──[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
文件系统。
┌──[root@liruilongs.github.io]-[~]
└─$mkdir /largepage
┌──[root@liruilongs.github.io]-[~]
└─$mount -t hugetlbfs none /largepage
如果在 NUMA 系统上,内核将大页划分到所有 NUMA 节点上,对应的静态大页参数需要分别设置,而不用设置全局参数
代码语言:javascript代码运行次数:0运行复制┌──[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, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-25,如有侵权请联系 cloudcommunity@tencent 删除进程内存内核异常linux认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知
写在前面
- 博文内容涉及 Linux 内存中
多级页表,缺页异常,TLB,以及大页
相关基本认知 - 理解不足小伙伴帮忙指正
对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
认识 Linux 内存构成:Linux 内存调优之页表、TLB、大页认知
上一篇博客和小伙伴们分享了内存中虚拟内存和物理内存相关知识,这里我们来看一下 页表,缺页异常,TLB 和大页相关知识
当启动一个程序时,会先给程序分配合适的虚拟地址空间
,但是不需要把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存
因为每个进程都维护着自己的虚拟地址空间,每个进程都有一个页表
来定位虚拟内存到物理内存的映射
,每个虚拟内存也在表中都有一个对应的条目
。
这里的页表是进程用于跟踪虚拟内存到物理内存的映射,那么实际的数据结构是什么的?
页表
如果每个进程都分配一个大的页表,64位系统 理论虚拟地址空间为2^64
字节,但实际 Linux 系统通常采用48
位有效虚拟地址
┌──[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=512
个PTE索引页(一个索引页存放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
命令查看当前系统存在缺页异常的进程
的排序
┌──[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
来查看指定命令,进程的 缺页异常情况
┌──[root@liruilongs.github.io]-[~]
└─$perfstat -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
中查询是否有缓存,如果有的话会直接获取,没有的话,走上面缺页异常的流程
进程访问虚拟地址 → MMU 查询 TLB → [命中 → 直接获取物理地址]
│
└→ [未命中 → 查询页表 → 权限检查 → 缺页处理(可选)→ 生成物理地址 → 更新 TLB]
│
└→ 访问物理内存
所以 TLB
命中率直接影响程序效率。若 TLB 未命中(Miss)
,需通过页表遍历获取物理地址,导致额外延迟(通常是 TLB 命中时间的数十倍)
,从内存加载页表项,并更新TLB缓存(可能触发条目替换,如LRU算法)
可以通过 perf stat
命令来查看某一个命令或者进程的 TLB
命中情况
┌──[root@liruilongs.github.io]-[~]
└─$perfstat -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>
的方式
┌──[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
透明大页用于合并传统内存页
代码语言:javascript代码运行次数:0运行复制┌──[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
下面为透明大页相关配置
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$ls /sys/kernel/mm/transparent_hugepage/
defrag enabled hpage_pmd_size khugepaged/ shmem_enabled use_zero_page
enabled 用于配置是否开启 THP
代码语言:javascript代码运行次数:0运行复制┌──[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 以稳定性能:
代码语言:javascript代码运行次数:0运行复制# 临时禁用
echo never > /sys/kernel/mm/transparent_hugepage/enabled
使用 grubby 更新内核启动参数,grubby 用于 动态修改内核启动参数 或 设置默认内核,无需手动编辑配置文件
代码语言:javascript代码运行次数:0运行复制# 永久禁用
┌──[root@liruilongs.github.io]-[~]
└─$grubby --update-kernel=ALL --args="transparent_hugepage=never"
┌──[root@liruilongs.github.io]-[~]
└─$reboot
确认配置
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
┌──[root@liruilongs.github.io]-[~]
└─$
再次查看透明大页使用情况
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$grep AnonHugePages /proc/meminfo
AnonHugePages: 0 kB
┌──[root@liruilongs.github.io]-[~]
└─$
shmem_enabled 用于配置 共享内存(如 tmpfs、共享匿名映射)是否启用透明大页(THP)
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
always within_size advise [never] deny force
这里的 never
表示完全禁用共享内存的透明大页。常用于数据库(如 Oracle、MySQL)或高延迟敏感型应用,避免因动态内存合并引发性能抖动
透明大页会涉及到一个进程 khugepaged
,khugepaged
是 Linux 内核的一部分,负责处理透明大页(Transparent HugePages, THP)的管理
。透明大页是内核自动将小页合并为大页以提升性能的机制,而 khugepaged 就是负责这个合并过程的守护进程。自动扫描内存区域,寻找可以合并的小页,并尝试将它们转换为透明大页。此过程在后台静默运行,无需应用程序显式请求。
┌──[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
的扫描频率,合并阈值
等可以通过下面的文件修改
┌──[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 预先分配的,这部分内存会被锁定,普通进程无法使用
,所以配置需要考虑清楚
┌──[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
┌──[root@liruilongs.github.io]-[~]
└─$sysctl -w vm.nr_hugepages=50
vm.nr_hugepages = 50
确认配置
代码语言:javascript代码运行次数:0运行复制┌──[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
代码语言:javascript代码运行次数:0运行复制┌──[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
内核的公共启动参数(包括默认内核和恢复模式内核)
┌──[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
标签
┌──[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 配置文件。重启系统使配置生效。
┌──[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
,这是什么原因,前面我们讲,静态大页会直接分配内存,即只有配置就会位于常驻内存,当系统内存没有配置的静态大页大时,系统会自动减少
┌──[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 命令查看内存使用情况验证这一点
代码语言:javascript代码运行次数:0运行复制┌──[root@liruilongs.github.io]-[~]
└─$free -g
total used free shared buff/cache available
Mem: 7 5 0 0 0 1
Swap: 0 0 0
通过临时修改内核参数调整静态大页数目(实际调整需要考虑静态大页是否使用)
代码语言:javascript代码运行次数:0运行复制┌──[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
确认配置是否生效
代码语言:javascript代码运行次数:0运行复制┌──[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
文件系统。
┌──[root@liruilongs.github.io]-[~]
└─$mkdir /largepage
┌──[root@liruilongs.github.io]-[~]
└─$mount -t hugetlbfs none /largepage
如果在 NUMA 系统上,内核将大页划分到所有 NUMA 节点上,对应的静态大页参数需要分别设置,而不用设置全局参数
代码语言:javascript代码运行次数:0运行复制┌──[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, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-04-25,如有侵权请联系 cloudcommunity@tencent 删除进程内存内核异常linux本文标签: 认识 Linux 内存构成Linux 内存调优之页表TLB缺页异常大页认知
版权声明:本文标题:认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/jiaocheng/1747518040a2170281.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论