2. 进程地址空间:虚拟内存与物理内存映射、页表机制、TLB原理

进程地址空间是操作系统为每个进程提供的抽象内存视图。它让每个进程都以为自己独占了从 0x0 到 0xFFFFFFFF(32位)或更大范围的连续内存空间,而实际上这些虚拟地址需要通过硬件和内核的协作,映射到分散的物理内存页上。理解这一层映射关系,是掌握 VMA 操作和 mmap 接口调用的基石。

2.1 虚拟内存与物理内存映射

虚拟内存(Virtual Memory, VM)是操作系统提供的一种内存管理技术,它通过将进程使用的虚拟地址(VA)转换为物理地址(PA),实现了以下关键目标:

  • 隔离性:每个进程拥有独立的虚拟地址空间,无法直接访问其他进程的内存。
  • 安全性:内核空间与用户空间分离,用户态程序无法篡改内核数据。
  • 高效利用:物理内存可以按需分配,不常用的内存页可以被换出到磁盘(Swap)。
  • 大地址空间:即使物理内存很小,进程也能使用远超物理内存大小的虚拟地址范围。
核心概念: 虚拟地址空间被划分为固定大小的“页”(Page,通常为 4KB),物理内存也被划分为同样大小的“页框”(Page Frame)。映射的基本单位就是页。

下图展示了虚拟地址到物理地址的映射关系,以及缺页中断的处理流程:

虚拟内存到物理内存映射与缺页中断流程 进程虚拟地址空间 页 0 (VA: 0x0000) 页 1 (VA: 0x1000) 页 2 (VA: 0x2000) ... 页 N (VA: 0xN000) 未映射区域 (可能触发缺页) 页表映射 物理内存 (DRAM) 页框 0 (PA: 0x0000) 页框 1 (PA: 0x1000) 页框 2 (PA: 0x2000) ... 页框 M (PA: 0xM000) 空闲页框 磁盘交换区 (Swap) 换出页 A 换出页 B ... 缺页中断 (Page Fault) 换入 换出
实战提示: 当进程访问一个尚未分配物理页框的虚拟地址时,CPU 会触发缺页异常(Page Fault)。内核的缺页中断处理程序会检查该地址是否属于某个 VMA。如果属于,则分配物理页框并建立映射;如果不属于,则向进程发送 SIGSEGV 信号(段错误)。

2.2 页表机制

页表(Page Table)是操作系统和硬件(MMU,内存管理单元)共同维护的数据结构,用于存储虚拟页号(VPN)到物理页框号(PFN)的映射关系。每个进程都拥有自己的页表。

2.2.1 多级页表结构

为了节省内存,现代操作系统(如 Linux)采用多级页表。以 x86-64 架构的 4 级页表为例:

级别 名称 索引位数 页表项大小 描述
L1 PGD (Page Global Directory) 9 bits 8 bytes 顶级页表,每个进程一个
L2 P4D (Page 4th Directory) 9 bits 8 bytes 第二级,仅在 5 级页表启用时存在
L3 PUD (Page Upper Directory) 9 bits 8 bytes 第三级
L4 PMD (Page Middle Directory) 9 bits 8 bytes 第四级
L5 PTE (Page Table Entry) 9 bits 8 bytes 最后一级,指向物理页框

一个 64 位虚拟地址被分解为多个部分:

| 63 ... 48 | 47 ... 39 | 38 ... 30 | 29 ... 21 | 20 ... 12 | 11 ... 0 |
|  符号扩展  |  PGD索引  |  PUD索引  |  PMD索引  |  PTE索引  | 页内偏移 |
|  16 bits   |  9 bits   |  9 bits   |  9 bits   |  9 bits   | 12 bits  |
关键点: 多级页表的核心优势在于“按需分配”。如果一个进程只使用了虚拟地址空间的一小部分,那么只有对应的 PGD、PUD、PMD 和 PTE 表会被创建,从而大幅节省内存。例如,一个只使用低 2MB 地址空间的进程,可能只需要 1 个 PGD 表、1 个 PUD 表、1 个 PMD 表和 1 个 PTE 表,总共约 32KB,而不是 512GB 的完整单级页表。

下图展示了 4 级页表的地址翻译过程:

x86-64 4级页表地址翻译过程 虚拟地址 (48 bits) PGD索引 (9 bits) PUD索引 (9 bits) PMD索引 (9 bits) PTE索引 (9 bits) 页内偏移 (12 bits) PGD PUD PMD PTE 物理页框 + 页内偏移

2.2.2 页表项(PTE)标志位

每个页表项不仅包含物理页框号,还包含一组标志位,用于控制页面的访问权限和状态:

标志位 名称 作用
P Present 页面是否在物理内存中。为 0 时访问会触发缺页中断。
R/W Read/Write 页面是否可写。为 0 时只读,写入会触发保护错误。
U/S User/Supervisor 页面是否允许用户态访问。为 0 时仅内核态可访问。
A Accessed 页面是否被访问过。由硬件自动设置,内核用于页面置换算法。
D Dirty 页面是否被写入过。用于判断换出时是否需要写回磁盘。

2.3 TLB 原理

TLB(Translation Lookaside Buffer,转译后备缓冲器)是 CPU 内部的一个硬件缓存,用于加速虚拟地址到物理地址的转换。由于页表存储在内存中,每次地址翻译都需要多次内存访问(多级页表需要 4-5 次),TLB 通过缓存最近使用的页表项来避免这些昂贵的访问。

2.3.1 TLB 工作流程

  1. TLB 查找:CPU 发出虚拟地址后,MMU 首先在 TLB 中查找对应的 VPN。
  2. TLB 命中:如果找到,直接获取物理页框号,组合页内偏移得到物理地址。耗时约 1 个 CPU 周期。
  3. TLB 未命中:如果未找到,MMU 需要遍历页表(硬件遍历或软件遍历),将结果填入 TLB,然后重试。耗时约 10-100 个 CPU 周期。
性能关键: TLB 命中率直接影响程序性能。对于内存访问密集型的应用(如数据库、大数据处理),TLB 未命中会导致显著的性能下降。Linux 内核通过“大页”(Huge Pages)技术,将页大小从 4KB 提升到 2MB 或 1GB,从而减少页表项数量,提高 TLB 覆盖范围。

下图展示了 TLB 在地址翻译中的位置和作用:

TLB 在地址翻译中的位置 CPU 虚拟地址 TLB (硬件缓存) 命中 未命中 物理地址 (直接访问内存) 物理内存 页表遍历 (内存访问) 回填 TLB

2.3.2 TLB 刷新与上下文切换

当发生进程上下文切换时,由于不同进程使用不同的页表,TLB 中的缓存项对于新进程是无效的。因此,操作系统必须刷新 TLB。刷新策略有两种:

策略 描述 优点 缺点
完全刷新 切换进程时,清空整个 TLB 实现简单,保证正确性 新进程启动时 TLB 未命中率高
地址空间标识符(ASID) TLB 项附带 ASID 标记,不同进程的项可以共存 上下文切换时无需刷新,减少 TLB 未命中 硬件需要支持 ASID,实现复杂
实战关联: 在编写高性能网络服务器或数据库时,频繁的进程/线程上下文切换会导致 TLB 刷新,进而引发性能抖动。使用线程(共享同一地址空间)而非进程,或者使用 mmap 映射大块内存并配合大页,可以有效缓解 TLB 压力。

2.4 本章小结

  • 虚拟内存 通过页表映射到物理内存,实现了进程隔离、安全保护和内存高效利用。
  • 多级页表 按需分配页表层级,大幅节省了页表占用的物理内存。
  • 页表项标志位 控制页面的访问权限、存在状态和脏页标记,是内核内存管理的基础。
  • TLB 作为硬件缓存,加速地址翻译过程。理解 TLB 命中/未命中行为,对于优化内存密集型程序至关重要。