虚拟内存与分页机制:从 MMU 到 TLB

虚拟内存是操作系统中最精妙的设计之一。它让程序可以方便地使用连续地址空间进行编码,而无需关心复杂多变的物理内存布局。同时,它也为操作系统调度、进程隔离和内存安全提供了底层支撑。从最早的段页式管理,到现代 x86-64 的多级页表,虚拟内存始终是操作系统与硬件交互最核心的环节之一。

虚拟内存是操作系统中最精妙的设计之一。它让程序可以方便地使用连续地址空间进行编码,而无需关心复杂多变的物理内存布局。同时,它也为操作系统调度、进程隔离和内存安全提供了底层支撑。从最早的段页式管理,到现代 x86-64 的多级页表,虚拟内存始终是操作系统与硬件交互最核心的环节之一。

本文将从虚拟内存出现的初衷出发,逐步解剖地址翻译机制、页表结构、MMU 与 TLB 的工作原理,最后聚焦于 Linux 下的缺页处理、用户态接口与大页优化等进阶议题。


为什么是虚拟内存?

在虚拟内存问世之前,程序直接操作物理地址,导致大量问题。第一个问题是内存不足。年代久远的机器内存仅有几KB到几十KB,而程序的大小普遍超过硬件上限。没有虚拟内存,程序员需要手动将程序拆分成可以加载和卸载的块,工程成本极高。

第二大问题是进程隔离。如果每个程序都能访问全部物理内存,那么一个越界指针就可能在另一个程序的数据区中写入垃圾,造成安全灾难。通过虚拟内存,每个进程拥有自己独立的地址空间。在 64 位系统上,表面看进程独享整个 2^48 或 2^57 字节的巨大地址空间,互不干扰,然而实际映射到物理内存的仅是真正使用的那部分。

此外,虚拟内存天然支持共享。不同进程可以把自己的虚拟页映射到同一个物理页,从而实现共享库和热数据零拷贝。操作系统也可以将文件内容映射到进程的地址空间,即内存映射 I/O(mmap),使得文件的读写可以直接通过指针操作完成,免去 read/write 的系统调用开销。


地址翻译基础:从虚拟地址到物理地址

虚拟内存的核心思想是将所有地址空间划分为一个个大小相等的块。在虚拟地址这一侧,这些块叫做页(Page);在物理内存这一侧,这些块叫做页框/帧(Frame)。虚拟页和物理页框的大小完全一致,操作系统只需维护页到帧的映射关系,即可把虚拟地址转换为物理地址。

页的大小

不同架构下的默认页大小有所不同:

  • x86 / x86-64:传统上为 4KB。
  • Apple Silicon(M1/M2/M3 等):采用 16KB 页大小。
  • 大页:x86-64 支持 2MB 和 1GB 的巨型页,用于减少页表层级和 TLB 压力。

当 CPU 发出一个虚拟地址后,内存管理单元(MMU)会先将其拆成两部分:页号页内偏移。页号用于索引页表,找到对应的物理页框号;页内偏移则直接保持不变,因为页与帧大小相同,偏移在任何场景下都不需要修改。


页表结构:从单级到多级

页表是操作系统维护的映射表格,用于存储虚拟页到物理帧的对应关系。看似简单的结构,实际演化出了多种实现。

单级页表

在 32 位机器使用 4KB 页的场景下,虚拟地址的有效位为 32 位。其中页内偏移占 12 位(2^12 = 4096),页号占 20 位。这意味着页表最多需要 2^20 个条目,即 1,048,576 个。每个条目若占 4 字节,整个页表大小就是 4MB。

单级页表的结构极其简单:CPU 直接用页号做下标查表即可。但问题在于,每个进程都需要这样一张独立的页表。如果系统中有 100 个进程,即使每个进程只用几十 KB 内存,也需要 400MB 内存来存页表,浪费巨大。

多级页表

现代操作系统采用多级页表来压缩存储。以 x86-64 为例,使用 48 位虚拟地址和 4KB 页,分页结构为 4 级:

  • PML4(Page Map Level 4)
  • PDPT(Page Directory Pointer Table)
  • PD(Page Directory)
  • PT(Page Table)

每一级都占 9 位索引(因为页大小 4KB / 条目大小 8 字节 = 512 个条目,2^9),加上最低 12 位页内偏移,9 × 4 + 12 = 48,完美覆盖。在这种结构中,只有实际映射的虚拟地址区域才会在中间各级页表中占据条目,未使用的区域各级页表要么为空,要么根本不会被分配,极大节省了内存。

在支持超过 512GB 物理内存的系统上,Intel 和 AMD 后续还推出了5 级页表,进一步扩展虚拟地址空间到 57 位。

反置页表

PowerPC 和 IA-64 等架构采用了**反置页表(Inverted Page Table)**的思路。不同于按虚拟页号索引,反置页表按物理帧号索引,整个系统只有一张表,条目数等于物理页框的总数。查找时需要配合哈希表,将虚拟地址哈希到对应的物理帧槽位。这种方式在内存上极度节省,但硬件设计更为复杂。

页表条目的内容

无论采用哪种结构,每个页表条目(PTE)通常包含以下关键字段:

  • 物理帧号(PFN):实际的物理页框地址。
  • Present / Absent:当前页是否存在于物理内存中。若为 0,访问该页将触发缺页中断(Page Fault)
  • R/W:读写权限。0 表示只读,1 表示可读写。
  • U/S:用户态 / 内核态。用户进程不能访问标记为内核态的页。
  • Accessed:该页是否被访问过,供页面置换算法参考。
  • Dirty:该页是否被修改过。若换出时需要写回磁盘,只有脏页才需要写入;干净页可以直接丢弃。

MMU:硬件层面的翻译引擎

页表虽然是操作系统定义的结构,但地址翻译的执行者是硬件 —— 内存管理单元(MMU)。MMU 集成在 CPU 内部,负责将每个虚拟地址翻译成物理地址,再把物理地址放到地址总线上。

翻译流程

一条指令要访问内存时,完整的地址翻译流程如下:

  1. CPU 将虚拟地址交给 MMU。
  2. MMU 首先检查 TLB,看该虚拟地址的翻译是否已被缓存。
  3. 若 TLB 命中,MMU 直接将缓存的物理帧号与页内偏移拼接,得到物理地址。
  4. 若 TLB 未命中,MMU 执行页表遍历(Page Table Walk):依次读取 PML4、PDPT、PD、PT 四级页表,从内存中取出 PTE,计算物理地址,并将结果回填至 TLB。
  5. 如果页表中发现 Present 位为 0,MMU 无法完成翻译,于是触发缺页异常,控制权转交操作系统处理。

TLB:地址翻译的高速缓存

页表遍历的代价非常高。在 4 级页表架构下,一次 TLB 未命中可能需要访问 4 次主存,相当于数百个时钟周期的延迟。如果每次内存访问前都要来这么一套,CPU 性能将被拖垮。

**TLB(Translation Lookaside Buffer)**专门用于缓存最近的虚拟页到物理帧的翻译结果。它本质上是页表的硬件缓存,尺寸很小但访问速度极快 —— 通常只需几个时钟周期。TLB 的容量因设计而异,典型值在几十到几千个条目之间。Intel Skylake 的 L1 D-TLB 有 64 个条目,L2 TLB 则有 1536 个。

TLB 未命中

TLB miss 的处理有两种模式。硬件页表遍历由 MMU 自动读取页表、更新 TLB,CPU 无需介入,x86 架构采用的就是这种方式。**软件遍历(Software TLB refill)**则是在 TLB 未命中时触发异常,由操作系统代码去查找页表并把结果填入 TLB。MIPS 和早期 RISC 处理器多用这种模式,灵活性更高但开销也更大。

TLB 失效与 shootdown

TLB 中的条目不是永久有效的。当发生上下文切换时,新进程的地址空间完全不同,旧 TLB 条目没有参考价值,因此需要清空或重新标记。现代 CPU 通常给每个 TLB 条目附加一个进程标识符(ASID / PCID),这样切换进程时不必 Flush 整个 TLB,仅过滤不匹配的条目即可。

更复杂的是多核环境下的 TLB Shootdown。当某个 CPU 核心修改了页表(例如调用 munmap 或回收一页内存),所有其他核心中的对应 TLB 条目必须失效。发送方会通过核间中断(IPI)通知其他核心执行 INVLPG 等指令刷掉旧条目,这是一个需要谨慎处理的同步过程。


缺页中断与请求分页

虚拟内存的精髓在于按需分配。进程的页表在初始时大量 PTE 的 Present 位为 0,直到程序真正访问到某个虚拟页,才触发缺页中断,由操作系统分配物理页并建立映射。

缺页处理流程

MMU 发现 Present 位为 0 时,会触发异常。CPU 陷入内核态,执行操作系统的缺页处理函数。此时操作系统需要判断:

  • 该虚拟地址是否合法(在 VMA 范围内)?不合法则发送 SIGSEGV,杀死进程。
  • 访问权限是否匹配(例如写只读页)?不匹配则发送 SIGBUS / SIGSEGV。
  • 该页是否曾经换出到交换分区?如果是,需要分配一个物理页,从磁盘读取旧内容,然后恢复映射。
  • 该页是首次使用、且映射到零页或文件页?直接分配物理页并填入内容即可。

Major vs Minor Page Fault

  • Major Page Fault:需要磁盘 I/O,例如从可执行文件加载代码段,或从 swap 恢复换出页。延迟在毫秒级。
  • Minor Page Fault:不需要磁盘 I/O。例如首次写匿名内存时分配零页、修改 Copy-on-Write 页等。延迟常在微秒级。

通过 time -v ./program 可以观察到程序运行期间的 Major / Minor Fault 次数,是衡量内存性能的重要指标。

Copy-on-Write(COW)与 fork()

Linux 的 fork() 并不会复制父进程的整个地址空间。相反,父子进程共享相同的物理页,但双方的 PTE 都标记为只读。任何一方尝试写入时,MMU 触发缺页,操作系统检测到这是 COW 页后,分配新的物理页、复制旧页内容、更新 PTE 为可写,然后再让用户继续执行。这样只有真正被修改的页才会产生复制开销。

userfaultfd

从 Linux 4.3 开始,内核提供了 userfaultfd 系统调用,允许用户态程序直接处理缺页中断。这对于用户态驱动、用户态内存数据库以及用户态的虚拟机内存管理非常有用 —— 不用改内核代码,就能自定义页缺失行为。


Linux 下的诊断接口

Linux 提供了丰富的 /proc/sys 接口,便于排查内存相关问题。

/proc/[pid]/maps

该文件展示进程的虚拟内存布局。每一行代表一个连续的地址区间(VMA),包含权限、偏移、映射的文件等信息。解析它就能知道进程的堆、栈、代码段、映射库以及 mmap 区域的位置和属性。

/proc/[pid]/smaps

smapsmaps 的增强版。它不仅列出每个 VMA,还对每个区间给出了详细的统计:RSS、PSS(比例分配大小)、共享/私有干净页和脏页、Huge_pages 数量等。分析 smaps 可以精确判断进程到底用了多少物理内存、哪些部分是共享的、哪些模块消耗最大。

/proc/meminfo

系统级别内存状态仪表板。可以查看 MemTotal、MemFree、Active、Inactive、Dirty、Writeback、AnonPages、HugePages_Total 等全局指标。运维排查 OOM 或内存激增时,meminfo 往往是第一检查点。

pmap 命令

pmap <pid> 以人类可读的方式呈现进程的内存映射。它本质上是把 /proc/[pid]/maps 格式化输出,并附带每个映射区域占用的 RSS。排查内存泄漏时,pmap 配合定期采样可以快速定位哪个地址段在不断膨胀。


进阶话题:大页与缓存优化

标准 4KB 页在大内存场景下会产生极端的页表和 TLB 开销。例如一个 128GB 内存的进程,如果使用 4KB 页,页表本身是 256MB,而 TLB 条目需要数千万次才能让工作集顺利驻留。这催生了**大页(Huge Pages)**机制。

显式大页(Hugetlbfs)

Linux 支持从启动参数 hugepages= 或运行时 sysctl 分配固定数量的物理大页,然后通过 mmap 挂在 hugetlbfs 上。x86-64 支持 2MB 和 1GB 两种规格。大页的好处是显著减少页表层级和 TLB miss,适合数据库、Java 堆、KVM 虚拟机内存等需要大段连续内存的场景。缺点是可能影响内存分配的细粒度,增加内存碎片。

Transparent Huge Pages(THP)

THP 是 Linux 内核的透明大页机制。它在后台自动尝试把相邻的 4KB 匿名页合并为 2MB 大页,无需应用程序感知。然而 THP 的缺点是可能会在内存压力下触发大量逐页拆分和压缩操作,在某些延迟敏感场景(如 Redis、PostgreSQL 高并发操作)中造成性能抖动。因此,很多高性能中间件会在文档中明确建议关闭 THP。

页着色与缓存优化

现代 CPU 的缓存并非全相联,而是按组相联(Set-Associative)实现。虚拟地址到缓存组的映射关系由页内偏移决定。如果多个虚拟页映射到了同一个物理缓存组中,就会发生冲突缺失。为了对抗这个问题,操作系统可以采用**页着色(Page Coloring)**策略,在分配物理页框时尽量选择映射到不同缓存组的帧。虽然这会增加管理复杂度,但在某些高吞吐内存应用中可以 measurably 降低缓存冲突率。


结语

虚拟内存和分页机制是现代操作系统的基石之一。它不仅解决了物理内存不足、进程隔离和多进程共享等经典难题,还通过页表、MMU、TLB、缺页中断的精密配合,让整个系统表现得高效而安全。从单级页表到多级甚至反置页表,从单纯的分页到按需加载、Copy-on-Write、用户态缺页处理,再到大页优化和页着色 —— 这一领域的演进从未停止。

对开发者而言,理解 MMU 如何工作、TLB miss 的代价、Major / Minor Fault 的成因,不仅能帮助你写出性能更优的代码,也能让你在面对 OOM、内存泄漏或容器隔离问题时拥有更深的洞察力。

希望本文能为你揭开虚拟内存背后的运作原理。如果在实际项目中需要排查内存问题,/proc/[pid]/smapspmap 永远是最好的起点。


延伸阅读

  1. 深入理解 Linux 内核内存管理子系统 —— slab / buddy system
  2. Linux Kernel Documentation: Documentation/mm/
  3. Intel SDM Vol. 3A —— Memory Management
  4. Meltdown and Spectre:现代 CPU 侧信道攻击与页表级隔离手段(KAISER / KPTI)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 进程与线程:从 PCB 到内核调度实体
  2. 系统性能诊断与调优:strace、perf、bpftrace
  3. 现代高性能 IO:epoll、io_uring 与异步 IO