认识 Linux 内存构成:Linux 内存调优之页表、TLB、缺页异常、大页认知
对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
写在前面
- 博文内容涉及 Linux 内存中
多级页表,缺页异常,TLB,以及大页
相关基本认知 - 理解不足小伙伴帮忙指正
对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
认识 Linux 内存构成:Linux 内存调优之页表、TLB、大页认知
上一篇博客和小伙伴们分享了内存中虚拟内存和物理内存相关知识,这里我们来看一下 页表,缺页异常,TLB 和大页相关知识
当启动一个程序时,会先给程序分配合适的虚拟地址空间
,但是不需要把所有虚拟地址空间都映射到物理内存,而是把程序在运行中需要的数据,映射到物理内存,需要时可以再动态映射分配物理内存
因为每个进程都维护着自己的虚拟地址空间,每个进程都有一个页表
来定位虚拟内存到物理内存的映射
,每个虚拟内存也在表中都有一个对应的条目
。
这里的页表是进程用于跟踪虚拟内存到物理内存的映射,那么实际的数据结构是什么的?
页表
如果每个进程都分配一个大的页表,64位系统 理论地址空间为2^64
字节,但实际 Linux 系统通常采用48
位有效虚拟地址
1 | ┌──[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
命令查看当前系统存在缺页异常的进程
的排序
1 | ┌──[root@liruilongs.github.io]-[~] |
也可以通过 perf stat
来查看指定命令,进程的 缺页异常情况
1 | ┌──[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 | 进程访问虚拟地址 → MMU 查询 TLB → [命中 → 直接获取物理地址] |
所以 TLB
命中率直接影响程序效率。若 TLB 未命中(Miss)
,需通过页表遍历获取物理地址,导致额外延迟(通常是 TLB 命中时间的数十倍)
,从内存加载页表项,并更新TLB缓存(可能触发条目替换,如LRU算法)
可以通过 perf stat
命令来查看某一个命令或者进程的 TLB
命中情况
1 | ┌──[root@liruilongs.github.io]-[~] |
- dTLB-loads 表示数据地址转换(Data TLB)的加载次数,
- dTLB-load-misses 表示未命中次数
- iTLB-loads 表示指令地址转换(Instruction TLB)的加载次数
- iTLB-load-misses 指令 TLB 未命中次数为 0,说明所有指令访问均命中 TLB 缓存。
查看指定进程的命中情况使用 -p <pid>
的方式
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 | ┌──[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 | ┌──[root@liruilongs.github.io]-[~] |
enabled 用于配置是否开启 THP
1 | ┌──[root@liruilongs.github.io]-[~] |
禁用透明大页 某些场景(如数据库)建议禁用 THP 以稳定性能:
1 | # 临时禁用 |
使用 grubby 更新内核启动参数,grubby 用于 动态修改内核启动参数 或 设置默认内核,无需手动编辑配置文件
1 | # 永久禁用 |
确认配置
1 | ┌──[root@liruilongs.github.io]-[~] |
再次查看透明大页使用情况
1 | ┌──[root@liruilongs.github.io]-[~] |
shmem_enabled 用于配置 共享内存(如 tmpfs、共享匿名映射)是否启用透明大页(THP)
1 | ┌──[root@liruilongs.github.io]-[~] |
这里的 never
表示完全禁用共享内存的透明大页。常用于数据库(如 Oracle、MySQL)或高延迟敏感型应用,避免因动态内存合并引发性能抖动
透明大页会涉及到一个进程 khugepaged
,khugepaged
是 Linux 内核的一部分,负责处理透明大页(Transparent HugePages, THP)的管理
。透明大页是内核自动将小页合并为大页以提升性能的机制,而 khugepaged 就是负责这个合并过程的守护进程。自动扫描内存区域,寻找可以合并的小页,并尝试将它们转换为透明大页。此过程在后台静默运行,无需应用程序显式请求。
1 | ┌──[root@liruilongs.github.io]-[/sys/devices/system/node] |
它会尝试将多个常规小页(4KB)合并成 大页(2MB 或 1GB),以减少页表项数量
,从而提升内存访问性能。
控制 khugepaged
的扫描频率,合并阈值
等可以通过下面的文件修改
1 | ┌──[root@liruilongs.github.io]-[/sys/devices/system/node] |
静态大页
静态大页需要单独配置,使用 sysctl
修改内核参数,可以设置分配的静态大页的数量
,大页内存是系统启动时或通过 sysctl 预先分配的,这部分内存会被锁定,普通进程无法使用
,所以配置需要考虑清楚
1 | ┌──[root@liruilongs.github.io]-[~] |
vm.nr_hugepages
:表示系统要预留的 大页数量,通过 -w 临时配置内核参数,配置大页数量为 50
1 | ┌──[root@liruilongs.github.io]-[~] |
确认配置
1 | ┌──[root@liruilongs.github.io]-[~] |
永久生效将配置写入 /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 | ┌──[root@liruilongs.github.io]-[~] |
GRUB_CMDLINE_LINUX
传递给所有 Linux
内核的公共启动参数(包括默认内核和恢复模式内核)
1 | ┌──[root@liruilongs.github.io]-[~] |
上面的配置 hugepages=10 hugepagesz=1G
,静态大页大小为 1G,数量为 10
需要说明的是,大页需要使用连续的内存空间,尽量设置永久规则,在开机时分配大页,如果系统已经运行了很久,大量的内存碎片,有可能无法分配大页,因为没有足够的连续内存空间
。
配置 1G 的静态大页需要CPU 支持,检查是否包含 pdpe1gb
标签
1 | ┌──[root@liruilongs.github.io]-[~] |
修改之后使用 grub2-mkconfig
生成了新的 GRUB 配置文件。重启系统使配置生效。
1 | ┌──[root@liruilongs.github.io]-[~] |
确认配置,可以看到 Hugepagesize
是1G,但是 nr_hugepages
大小为 5
,并不是我们配置的 10
,这是什么原因,前面我们讲,静态大页会直接分配内存,即只有配置就会位于常驻内存,当系统内存没有配置的静态大页大时,系统会自动减少
1 | ┌──[root@liruilongs.github.io]-[~] |
我们可以使用 free 命令查看内存使用情况验证这一点
1 | ┌──[root@liruilongs.github.io]-[~] |
通过临时修改内核参数调整静态大页数目(实际调整需要考虑静态大页是否使用)
1 | ┌──[root@liruilongs.github.io]-[~] |
确认配置是否生效
1 | ┌──[root@liruilongs.github.io]-[~] |
为了让进程可以使用大页,进程必须进行系统函数调用,可以调用 mmap()
函数,或者 shmat()
函数,又或者是 shmget()
函数。如果进程使用的是 mmap()
系统函数调用,则必须挂载-个 hugetlbfs
文件系统。
1 | ┌──[root@liruilongs.github.io]-[~] |
如果在 NUMA 系统上,内核将大页划分到所有 NUMA 节点上,对应的静态大页参数需要分别设置,而不用设置全局参数
1 | ┌──[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、大页认知/