知行LEARNING HANDBOOK
目录 · N06 虚拟内存、物理页面与多级页表
学习手册/计算机基础/虚拟内存与页表
N0610 分钟更新于 2026-08-20

虚拟内存、物理页面与多级页表

从进程为什么需要虚拟内存出发,理解物理页面、映射关系和多级页表。

virtual-memorypage-tableptecr3mmutlbphysical-page

建议先了解

  • 二进制与地址
  • 进程基础

1. 这篇笔记解决什么问题

本篇是本次对话中计算机系统部分的核心,回答:

  • page 与 physical page frame 是什么;
  • 页面管理元数据放在哪里;
  • 页表到底是程序还是数据结构;
  • 每个进程是否有独立页表;
  • 多级页表为什么存在;
  • L4/L3/L2/L1 在 RAM 中到底是什么;
  • L4 的 512 个 entry 是否全部真实存在;
  • 一个 entry 保存什么;
  • CR3 是什么;
  • CPU、MMU、OS 各自决定什么。

2. 前置知识

待补充:page fault、TLB、cache 的更完整背景。

3. 核心概念

3.1 虚拟页(Virtual Page)

进程虚拟地址空间按固定 page size 划分。

以 4 KiB 为例:

Virtual Page 0: 0x0000 - 0x0FFF
Virtual Page 1: 0x1000 - 0x1FFF
Virtual Page 2: 0x2000 - 0x2FFF
...

3.2 物理页框(Physical Page Frame)

物理地址空间/RAM 也按 page size 组织成 page frame:

Frame 0
Frame 1
Frame 2
...

虚拟页编号与物理页框编号不需要相同,也不要求连续对应。

3.3 页面本身与页面元数据

一个物理页框主要用于存实际内容:

程序代码
程序数据
stack/heap 数据
文件缓存
内核数据
页表本身
...

“这个页属于谁、是否 dirty、引用数多少”等管理信息通常不要求固定塞在 page 自身开头。

本次对话区分了两类重要元数据:

  1. PTE:描述虚拟地址映射与硬件权限;
  2. kernel page descriptor:描述物理页框的内核管理状态,例如引用、LRU、allocator 等。

Linux 中可联系到 struct page 概念,但其具体字段强烈依赖版本和用途。

3.4 页表(Page Table)

页表不是一个独立程序,而是:

存放在 RAM 中、由 OS 建立和维护、由 CPU/MMU 按架构规则读取的地址转换数据结构。

核心:

Virtual Page Number (VPN)
          ↓
         PTE
          ↓
Physical Frame Number (PFN)

3.5 页表项(Page Table Entry,PTE)

一个 entry 通常同时保存:

  • 下一层页表或最终物理页框的地址相关 bits;
  • Present/Valid;
  • Read/Write;
  • User/Supervisor;
  • Execute/NX;
  • Accessed;
  • Dirty;
  • cache 属性;
  • 其他架构规定或 OS 可用 bits。

具体 bit 布局必须看对应 CPU architecture 与 paging mode。

3.6 MMU

内存管理单元(Memory Management Unit,MMU)负责执行地址转换。

职责分工:

CPU / ISA
  → 规定页表格式、层级、地址位拆分、entry bit 语义、page walk 规则

OS
  → 创建页表、填映射、设置权限、处理 page fault、切换地址空间

MMU
  → 按 CPU 规定的规则读取页表并完成 VA → PA

3.7 CR3

CR3 是 x86/x86-64 的特殊控制寄存器,不是普通通用寄存器。

在经典 x86-64 四级页表模型中,可先理解为:

CR3
 ↓
当前地址空间的顶级 PML4/L4 页表物理地址相关信息

它是特权状态,普通用户态程序不能任意修改。

ARM64 不叫 CR3,会使用类似 TTBR 的 translation table base register;思想相近但格式不同。

4. 直观理解

单级页表最直观:

PageTable[VPN]

CPU 可以直接计算:

PTEAddress=PageTableBase+VPN×sizeof(PTE)PTEAddress = PageTableBase + VPN \times sizeof(PTE)

优点是快;问题是即使某个 VPN 根本没映射物理页,它对应的数组槽位仍然得存在。

多级页表就是把一个巨大稠密数组变成“按需展开的固定扇出树”。每一层仍可直接用地址位做数组索引,但没用到的分支不创建下一级表。

5. 工作原理

5.1 为什么单级页表太大

经典模型:

48-bit virtual address
4 KiB page
8-byte PTE

因为:

4KiB=2124KiB = 2^{12}

所以虚拟页数:

248/212=2362^{48}/2^{12}=2^{36}

若单级页表为完整数组:

236×8=239bytes=512GiB2^{36}\times 8 = 2^{39} bytes = 512 GiB

即使进程实际只用了几十 MiB,也必须为理论所有 VPN 预留 entry,代价不可接受。

5.2 四级页表如何拆分地址

经典 48-bit x86-64 + 4 KiB page:

47        39 38        30 29        21 20        12 11       0
┌──────────┬────────────┬────────────┬────────────┬───────────┐
│ L4 index │ L3 index   │ L2 index   │ L1 index   │ offset    │
│ 9 bit    │ 9 bit      │ 9 bit      │ 9 bit      │ 12 bit    │
└──────────┴────────────┴────────────┴────────────┴───────────┘

每层 9 bit:

29=5122^9 = 512

若 entry 8B:

512×8B=4096B512 \times 8B = 4096B

所以一张页表正好占一个 4 KiB page。

5.3 每一级覆盖多少地址

一个 L1 entry → 4 KiB
一张 L1 table → 512 × 4 KiB = 2 MiB
一张 L2 table → 512 × 2 MiB = 1 GiB
一张 L3 table → 512 × 1 GiB = 512 GiB
一张 L4 table → 512 × 512 GiB = 256 TiB

5.4 多级页表到底省在哪里

这是本次对话中最重要的纠正。

一张 L4 一旦存在:

512 个 entry 槽位全部真实存在。

例如:

L4
├── entry[0]   present=0
├── entry[1]   present=0
├── entry[2]   present=1 ──→ 一张 L3
├── entry[3]   present=0
└── ...

entry[0] 本身的 8B 仍然占内存;真正不存在的是它指向的下一级页表。

如果一个 L4 entry 无效,它对应的整片 512 GiB 虚拟地址范围都可以不创建下方 L3/L2/L1 子树。

因此:

多级页表并不是让“完全填满时”的页表理论规模变小,而是让稀疏地址空间可以按需展开。

5.5 一张真实 L4 在 RAM 中如何存在

假设 L4 物理基地址:

0x0000000123400000

内存中就是一个真实 4096B 页:

offset       内容

+0x000       entry[0]    8B
+0x008       entry[1]    8B
+0x010       entry[2]    8B
...
+0x128       entry[37]   8B
...
+0xFF8       entry[511]  8B

entry 地址:

Address(entry[i])=L4Base+i×8Address(entry[i]) = L4Base + i\times8

这就是硬件能够快速直接索引的原因。

5.6 一个 L4 entry 保存什么

简化模型:

63                                               12 11             0
┌────┬────────────────────────────────────────────┬─────────────────┐
│ NX │ 下一级 L3 页表物理地址相关 bits            │ flags           │
└────┴────────────────────────────────────────────┴─────────────────┘

假设某 entry 概念值:

0x0000000456700007

可以粗略拆成:

0x0000000456700000 → 下一级页表物理地址部分
0x7                → 一些低位 flags

[!WARNING] 上图只是教学简化。真实 PML4E/PTE 的 reserved bits、地址 bits、NX 等布局必须查对应 Intel/AMD 架构手册,不能从示例直接推导。

5.7 为什么不直接“只创建用到的 PTE”而不用多级页表

如果把所有有效 VPN-PTE 对稀疏存放:

VPN 123 → PTE
VPN 999999 → PTE
...

新的问题是:

MMU 拿到一个 VPN 后,怎么快速找到那个 PTE?

若不是完整数组,就需要 hash/tree/inverted table 等额外查找结构。

多级页表是一种工程折中:

地址 bits 分段
 ↓
每层仍然是固定数组直接索引
 ↓
只有需要的下级表才分配

因此兼顾规则硬件查找与稀疏空间节省。

5.8 每个进程的页表

原则上每个进程/地址空间拥有自己的页表体系。

Process A
VA 0x1000 → PageTable_A → PFN 100

Process B
VA 0x1000 → PageTable_B → PFN 928

因此同一个虚拟地址可以同时在不同进程中存在,却映射不同物理页。

内核空间映射是否共享、如何隔离,取决于具体 OS 与安全设计,不能简单说“每一项都完全不同”。

5.9 TLB

完整 page-table walk 很贵,因此 CPU 使用转换后备缓冲区(Translation Lookaside Buffer,TLB)缓存:

VPN → PFN

流程:

Virtual Address
      ↓
     TLB
   ↙     ↘
 hit     miss
 ↓        ↓
PFN    L4→L3→L2→L1 walk

6. 示例

示例 1:8 MiB 连续映射

一张 L1 可覆盖 2 MiB,因此 8 MiB 连续地址约需 4 张 L1。

若它们落在相同上级范围内,概念上可能为:

1 × L4
1 × L3
1 × L2
4 × L1

共 7 张 4 KiB 页表:

7×4KiB=28KiB7\times4KiB=28KiB

这个例子说明稀疏/按需分配的价值。

示例 2:同一个 VA

Process A:
CR3 → PageTable_A
VA 0x4000 → Frame 10

Process B:
CR3 → PageTable_B
VA 0x4000 → Frame 900

说明:虚拟地址的含义取决于当前地址空间页表。

7. 代码、命令或公式

4 KiB page:

4096=2124096=2^{12}

因此低 12 bit 为 page offset。

最终物理地址可概念化为:

PA=(PFN12)    offsetPA = (PFN \ll 12)\;|\;offset

单级数组 PTE 位置:

PTEAddress=Base+VPN×PTEsizePTEAddress = Base + VPN\times PTEsize

这直接解释了为什么单级完整数组必须为所有 VPN 保留槽位。

8. 容易混淆的概念

概念 A概念 B核心区别
virtual pagephysical page frame前者属于进程地址空间,后者属于物理地址空间
pagePTEpage 存实际内容,PTE 存映射和权限
page table程序页表是数据结构,不是独立运行程序
PTEkernel page descriptor前者给地址翻译/权限用,后者给 OS 管理物理页用
L4 entry 不存在L4 entry present=0L4 槽位存在,只是值表示无效;下一级表才不创建
CR3普通寄存器CR3 是特权控制寄存器,提供页表根相关信息
CPU 决定格式OS 决定内容CPU 规定页表语法,OS 填具体映射
TLBpage tableTLB 是缓存,page table 是权威映射结构

9. 常见误区

误区:L4 中没用的 entry 本身不占空间

错误原因:把“下级表按需创建”误认为“上级数组槽位也动态消失”。

正确理解:一张 L4 固定 512×8B=4KiB,全部槽位存在;未使用 entry 只是在值上标记无效。

如何验证:计算一张页表的固定大小。

误区:多级页表的理论最大内存一定比单级页表小

错误原因:忽略多级页表主要解决稀疏问题。

正确理解:若整个地址空间全部映射,多级页表仍很大;日常收益来自大量无效分支不展开。

误区:完全不需要多级页表,只给有用 VPN 创建 entry

错误原因:没有解决“硬件如何定位稀疏 entry”。

正确理解:稀疏结构必须再引入 hash/tree 等索引;多级页表本身就是硬件友好的稀疏 radix tree。

误区:页表格式是操作系统设计的

错误原因:忽略 MMU 直接按硬件格式读取页表。

正确理解:ISA/CPU 规定 entry 组织、bit 语义和 walk 规则;OS 负责按规则填内容。

误区:物理页的前几个字节一定保存页面元数据

错误原因:把对象 header 模型套到 page frame。

正确理解:页框主要放实际内容;PTE 和 OS 的 page metadata 通常存于其他内存结构。

10. 与其他知识的关系

待建链接:TLB

待建链接:Page Fault 与 Demand Paging

待建链接:Linux 物理页管理与 Buddy Allocator

11. 可以亲手完成的验证

实验 A:手算覆盖范围

  1. page = 4 KiB;
  2. entry = 8B;
  3. table = 4 KiB;
  4. 每表 entry 数 = 4096/8=512;
  5. 依次计算 L1/L2/L3/L4 覆盖范围。

预期:

L1 table → 2 MiB
L2 table → 1 GiB
L3 table → 512 GiB
L4 table → 256 TiB

实验不能证明:实际 CPU 当前一定使用 48-bit 四级模式。

实验 B:观察进程虚拟地址

编写程序打印全局、stack、heap 地址,多次运行比较。

预期:现代启用 ASLR 的系统中部分虚拟地址会变化。

实验不能证明:仅打印地址无法直接看到真实 PTE bit 或完整 page walk。

12. 尚未解决的问题

  • x86-64 PML4E/PDPTE/PDE/PTE 的完整逐 bit layout 未整理。
  • 5-level paging(LA57)未展开。
  • ARM64 TTBR 与 4KiB/16KiB page table 模式未展开。
  • Apple Silicon/macOS 的具体页大小与页表层级尚未在本笔记核验。
  • Linux struct page 的当前字段和不同 union 用途未展开。
  • page fault、swap、copy-on-write、mmap、shared memory 尚未系统整理。
  • TLB shootdown 与 context switch 的细节未讨论。

13. 自测问题

  1. 为什么一张 L4 表的 512 个 entry 槽位必须全部存在?
  2. L4 某个 entry 为 invalid 时,真正省掉了哪些结构?
  3. 单级页表为什么会产生巨大的元数据开销?
  4. 多级页表为什么既能直接索引又能稀疏分配?
  5. CR3 在经典 x86-64 模型中的作用是什么?
  6. 为什么不同进程可以同时使用相同的虚拟地址?
  7. 页表格式为什么主要由 CPU/ISA 决定?
  8. TLB 为什么必要?
参考答案
  1. 因为 L4 本身就是一张固定 4KiB 数组,512 个 8B entry 正好填满。
  2. 省掉对应 L3 以及可能进一步存在的 L2/L1 整棵子树。
  3. 单级数组为了 O(1) 直接索引,必须为所有可能 VPN 保留槽位,即使大多数未使用。
  4. 地址位直接作为每一级固定数组下标,同时只有用到的上级 entry 才分配下一级表。
  5. 提供当前地址空间顶级页表的物理地址相关信息,作为 MMU page walk 起点。
  6. 因为它们拥有不同页表,同一 VA 可以映射到不同 PFN。
  7. 因为 MMU 硬件直接解析页表,必须遵循硬件规定的 entry bit 和 walk 规则。
  8. 为缓存 VPN→PFN 结果,避免每次内存访问都完整走多级页表。

14. 一句话总结

多级页表本质上是一棵由 CPU 定义格式、OS 按需构建、MMU 硬件遍历的稀疏 radix tree:顶级表槽位固定存在,但未使用分支无需继续分配,从而兼顾快速索引与稀疏内存占用。