第5讲 虚拟内存管理I

虚拟内存管理 I:从地址隔离到页表翻译

虚拟内存管理的第一层问题,并不是“物理内存不够时怎么办”,而是“程序为什么不能直接使用物理地址”。现代操作系统让每个进程运行在自己的虚拟地址空间中,程序看到的是虚拟地址,真正访问内存时再由硬件和操作系统配合,把虚拟地址翻译成物理地址。

这套机制的核心由四个部分组成:

  • 操作系统:建立和维护地址翻译规则。
  • 页表或段表:保存虚拟地址空间到物理内存的映射关系。
  • MMU:在 CPU 访存时执行地址翻译。
  • TLB:缓存近期翻译结果,降低查页表的成本。

❗ 核心结论: 虚拟内存不是单纯为了“扩大内存”,而是为了给每个进程提供独立、受保护、可管理的地址空间。

1. 为什么不能直接使用物理地址

物理内存就是机器上真实存在的内存条。程序代码、程序数据、操作系统代码和操作系统数据,最终都必须位于物理内存中,CPU 才能访问。

在早期单程序环境中,物理内存可以简单划分为:

1
2
3
物理内存
├── 操作系统
└── 一个应用程序

但在多程序系统中,多个应用会同时驻留在内存中:

1
2
3
4
5
物理内存
├── 操作系统
├── 应用 A
├── 应用 B
└── 应用 C

如果应用程序直接使用物理地址,会带来几个问题:

  • 缺乏隔离:一个程序可能访问或破坏另一个程序的数据。
  • 加载位置不稳定:程序被加载到哪里取决于当时物理内存的空闲情况。
  • 暴露真实内存布局:程序能够感知甚至推断系统和其他程序的位置。
  • 管理困难:操作系统难以灵活移动、共享或保护内存区域。

因此,虚拟内存首先解决的是地址抽象和隔离问题。程序使用虚拟地址,物理地址由系统在背后管理。


❗ 本节核心结论: 程序不应直接使用物理地址,因为物理地址暴露真实内存布局,并削弱进程隔离与系统管理能力。

2. 虚拟地址与地址翻译

虚拟内存抽象下,程序中看到的地址通常不是物理地址。例如程序打印出的指针值,本质上属于当前进程的虚拟地址空间。

一次普通内存访问可以概括为:

这张图强调:程序并不是直接拿物理地址访问内存,而是先经过 MMU 的地址翻译。

各组件的职责可以概括为:

组件 主要职责
CPU 执行指令,并发出虚拟地址
MMU 根据当前地址翻译规则,把虚拟地址翻译为物理地址
操作系统 创建、维护、切换段表或页表
页表/TLB 页表保存映射关系,TLB 缓存近期翻译结果

这里最重要的边界是:操作系统负责建表,MMU 负责查表。 MMU 并不会凭空创造映射关系,它只是使用当前进程对应的翻译规则。


❗ 本节核心结论: 地址翻译是硬件与操作系统合作完成的:操作系统建立规则,MMU 在访存路径上使用规则。

3. 虚拟地址不是全局地址

一个常见误区是:如果程序 A 知道程序 B 的虚拟地址,是否就能拿这个地址去查 B 的段表或页表,从而得到 B 的物理地址?

答案是:正常情况下不能。

原因在于虚拟地址不是全局地址。虚拟地址必须绑定到“当前是哪一个进程、当前使用哪一张地址翻译表”这个上下文中才有意义。

例如,同样的虚拟地址在两个进程中可能对应不同物理地址:

1
2
进程 A:虚拟地址 0x400000 → 物理地址 X
进程 B:虚拟地址 0x400000 → 物理地址 Y

当进程 A 正在运行时,MMU 使用的是进程 A 的段表或页表。即使 A 知道 B 中存在某个虚拟地址,A 自己访问这个数值时,硬件也会按照 A 的地址空间进行翻译,而不是按照 B 的地址空间进行翻译。

因此,虚拟内存隔离的关键不是“别人不知道你的虚拟地址”,而是:

  • 别人不能使用你的地址翻译规则。
  • 别人不能随便读取你的段表或页表。
  • 程序运行时,MMU 只使用当前上下文绑定的翻译表。

❗ 本节核心结论: 虚拟地址只有在特定进程地址空间中才有意义;单独知道别人的虚拟地址,并不能访问别人的物理内存。

4. 分段机制:直观但会产生外部碎片

分段机制是一种较早、较直观的虚拟内存组织方式。它把虚拟地址空间分成若干不定长段,例如:

  • 代码段
  • 数据段
  • 堆段
  • 栈段

分段机制下,虚拟地址可以理解为:

1
虚拟地址 = 段号 + 段内偏移

段表中保存:

1
段号 → 物理起始地址 + 段长度

若某段物理起始地址为 0x500000,段内偏移为 0x350,则物理地址为:

1
0x500000 + 0x350 = 0x500350

分段的优点是符合程序结构直觉:代码、数据、堆、栈本来就像不同区域。但它的主要问题是外部碎片

因为段的大小不固定,物理内存可能变成:

1
[段1][空洞][段3][空洞][段2]

这些空洞加起来可能很大,但单个空洞不够大时,新段仍然无法放入。这就是分段机制在内存利用率上的局限。

机制 管理单位 地址结构 优点 主要问题
分段 不定长段 段号 + 段内偏移 符合程序结构 外部碎片
分页 固定大小页 虚拟页号 + 页内偏移 管理规整,减少外部碎片 需要页表和地址翻译

❗ 本节核心结论: 分段的抽象很直观,但不定长段容易造成外部碎片,因此现代系统更依赖分页机制。

5. 分页机制:页号翻译,偏移不变

分页机制把虚拟内存和物理内存都切成固定大小的块:

  • 虚拟地址空间中的块称为虚拟页
  • 物理内存中的块称为物理页
  • 常见页大小是 4KB。

分页下,虚拟地址被拆成:

1
虚拟地址 = 虚拟页号 + 页内偏移

页表记录:

1
虚拟页号 → 物理页号

翻译完成后,物理地址为:

1
物理地址 = 物理页号 + 同一个页内偏移

这里最关键的是:地址翻译只改变页号,不改变页内偏移。

例如,假设页大小是 4KB,即 0x1000。虚拟地址:

1
0x12345

可以拆成:

1
2
虚拟页号 = 0x12
页内偏移 = 0x345

如果页表中有映射:

1
虚拟页号 0x12 → 物理页号 0xABC

那么物理地址就是:

1
0xABC000 + 0x345 = 0xABC345

页表并不是为每一个字节单独记录映射,而是为每个虚拟页记录它对应的物理页。页内第 837 个字节,映射后仍然是物理页内第 837 个字节。


❗ 本节核心结论: 页表保存的是“虚拟页号 → 物理页号”,不是逐字节的“虚拟地址 → 物理地址”;页内偏移保持不变。

6. 单级页表为什么会太大

单级页表可以想象成一个巨大数组:每个虚拟页号对应一个页表项。

它的空间计算公式是:

1
页表大小 = 虚拟页数量 × 每个页表项大小

其中:

1
虚拟页数量 = 虚拟地址空间大小 / 页大小

因此:

1
页表大小 = 虚拟地址空间大小 / 页大小 × 页表项大小

6.1 32 位地址空间示例

设定:

  • 虚拟地址空间:32 位,即 2^32 字节。
  • 页大小:4KB,即 2^12 字节。
  • 页表项大小:4 字节。

则虚拟页数量为:

1
2^32 / 2^12 = 2^20

页表大小为:

1
2^20 × 4B = 2^22B = 4MB

一个进程一张 4MB 页表,在 32 位系统中还勉强可以接受。

6.2 64 位地址空间示例

设定:

  • 虚拟地址空间:64 位,即 2^64 字节。
  • 页大小:4KB,即 2^12 字节。
  • 页表项大小:8 字节。

则虚拟页数量为:

1
2^64 / 2^12 = 2^52

页表大小为:

1
2^52 × 8B = 2^55B

换算为 GB:

1
2^55 / 2^30 = 2^25GB = 33,554,432GB

这个规模显然不可接受。问题的根源在于:单级页表需要为整个虚拟地址空间中的每个虚拟页准备页表项,即使绝大多数虚拟地址根本没有被使用。


❗ 本节核心结论: 单级页表的空间浪费来自“覆盖整个虚拟地址空间”;64 位地址空间下,这种设计会导致页表规模不可接受。

7. 多级页表:用更多查询步骤换更少空间

多级页表的基本思想是:不要为根本没有使用的虚拟地址区域分配页表。

单级页表像一个巨大数组:

1
虚拟页号 → 页表项

多级页表更像一棵按需展开的树:

这张图说明:如果某个地址区域没有被使用,对应的下级页表可以不存在,从而节省空间。

多级页表的代价是翻译变慢。如果没有 TLB,地址翻译可能需要逐级访问多张页表,最后才能得到物理页号。

因此,多级页表体现的是一个典型权衡:

方案 空间开销 翻译速度 核心特点
单级页表 查一次表较直接 为整个虚拟地址空间建表
多级页表 可能多次查表 只为使用到的区域分配下级页表

多级页表不是 MMU 自动创建的。它由操作系统在进程创建、程序加载或内存分配时按需创建和维护。MMU 的职责是在运行时按照页表结构逐级查询。


❗ 本节核心结论: 多级页表通过允许地址空间中存在“空洞”来节省空间,但代价是一次地址翻译可能需要多次查表。

8. AArch64 四级页表:9 + 9 + 9 + 9 + 12

以 AArch64 的常见设定为例:

  • 页大小:4KB。
  • 页内偏移位数:log2(4KB) = 12 位。
  • 有效虚拟地址:48 位。

因此:

1
48 位虚拟地址 = 36 位虚拟页号 + 12 位页内偏移

36 位虚拟页号被分成 4 级索引:

1
36 = 9 + 9 + 9 + 9

所以地址结构可以写成:

1
2
虚拟页号_0 | 虚拟页号_1 | 虚拟页号_2 | 虚拟页号_3 | 页内偏移
9 位 9 位 9 位 9 位 12 位

每一级 9 位,可以索引:

1
2^9 = 512

也就是说,每一级页表最多有 512 个页表项。

AArch64 四级页表的翻译过程可以概括为:

这张图强调:四级页表并不是四种不同地址,而是把虚拟页号分成四段,逐层缩小搜索范围,最后找到物理页号。


❗ 本节核心结论: 在 4KB 页、48 位有效虚拟地址下,AArch64 四级页表的地址拆分规律是 9 + 9 + 9 + 9 + 12

9. 页表项不仅保存物理页号

页表项的核心内容是物理页号,但它通常不只保存物理页号。它还包含权限和状态信息。

常见字段包括:

字段 作用
valid 位 表示页表项是否有效
AP 位 控制读写权限
XN 位 控制用户态是否禁止执行
PXN 位 控制内核态是否禁止执行
AF 位 Access Flag,可用于记录页面是否被访问
AttrIndx 指示内存类型,如普通内存或设备内存

因此,页表不只是“地址翻译表”,也是保护机制的一部分。MMU 在查页表时,不仅要得到物理页号,还可能检查当前访问是否合法。

当页表项无效、权限不足或执行控制不允许时,MMU 不应简单返回物理地址,而应触发异常,让操作系统处理。


❗ 本节核心结论: 页表项既表达地址映射,也表达访问权限、执行控制和内存属性;页表同时承担翻译与保护功能。

10. 页表使能:从物理寻址到虚拟寻址

CPU 刚启动时,系统通常处于较原始的物理寻址阶段。操作系统或底层系统软件初始化页表后,会配置控制寄存器,使地址翻译生效。

典型例子包括:

架构 控制方式
AArch64 设置 SCTLR_EL1M
x86_64 设置 CR0PG

可以简化理解为:

1
2
页表未使能:CPU 发出的地址直接当作物理地址
页表已使能:CPU 发出的地址先经 MMU 翻译,再访问物理内存

这说明虚拟内存不是只存在于软件概念中,它需要硬件控制位和页表基地址寄存器共同支持。


❗ 本节核心结论: 页表只有在硬件控制位开启后才参与访存路径;开启后,CPU 发出的地址会经过 MMU 翻译。

11. TLB:让多级页表重新变快

多级页表节省空间,但也增加了地址翻译成本。若每次访问内存都要查四级页表,那么一次普通访存可能变成:

1
2
3
4
5
查 L0 页表
查 L1 页表
查 L2 页表
查 L3 页表
真正访问数据

为降低这个成本,CPU 内部提供 TLB:

1
TLB = Translation Lookaside Buffer

它缓存的是近期使用过的地址翻译结果:

1
虚拟页号 → 物理页号

地址翻译过程可以概括为:

TLB 有效的原因是程序访问内存具有局部性:

局部性 含义 例子
时间局部性 刚访问过的地址,之后很可能再次访问 循环中反复访问同一变量
空间局部性 访问某地址后,附近地址也很可能被访问 顺序遍历数组

例如连续访问数组 a[i] 时,多个元素可能位于同一页内。只要这一页的映射已经在 TLB 中,访问同页其他元素时就可以复用同一条翻译结果。


❗ 本节核心结论: TLB 是页表查询结果的缓存,它利用程序访问局部性减少多级页表查询次数。

12. 进程切换、TLB 刷新与 ASID

TLB 缓存的是虚拟页号到物理页号的映射。但不同进程有不同页表,可能出现:

1
2
进程 A:虚拟页 0 → 物理页 1
进程 B:虚拟页 0 → 物理页 9

如果进程切换后仍错误复用旧进程的 TLB 项,就会把 A 的映射用于 B,造成严重错误。

最直接的解决方式是:每次进程切换都刷新 TLB。但这会丢失缓存,性能较差。

更好的办法是给 TLB 项加入地址空间标签。例如 AArch64 使用 ASID:

1
ASID = Address Space ID

TLB 项可理解为:

1
ASID + 虚拟页号 → 物理页号

这样,即使两个进程都有虚拟页 0,也能区分:

1
2
ASID 1 + 虚拟页 0 → 物理页 1
ASID 5 + 虚拟页 0 → 物理页 9

ASID 的意义是:把“当前属于哪个地址空间”这个上下文显式加入 TLB 项,从而减少不必要的全量刷新。


❗ 本节核心结论: ASID 让 TLB 项带上地址空间身份,避免不同进程的相同虚拟页号发生混淆。

13. MMU、操作系统、页表和 TLB 的职责边界

虚拟内存管理容易混淆的地方,是把“谁建立规则”和“谁使用规则”混为一谈。可以用下表区分:

角色 做什么 不做什么
操作系统 创建页表、分配页表页、填写 PTE、设置权限、切换页表 不在每次普通访存时手动翻译地址
MMU 根据当前页表和 TLB 执行地址翻译与权限检查 不自行决定哪些虚拟页应映射到哪些物理页
TLB 缓存近期翻译结果 不替代页表作为权威映射来源
CPU 执行指令并发起访存 不直接理解进程隔离策略

多级页表的创建流程可以简化为:

这张图说明:多级页表由操作系统按需创建;程序运行时,MMU 只是沿着已经建立好的结构查找。


❗ 本节核心结论: 操作系统负责构造地址空间,MMU 负责执行翻译,TLB 负责缓存结果;三者分工不能混淆。

14. 常见误区集中澄清

14.1 虚拟内存不是只为了解决“内存不够”

虚拟内存更基础的目标是隔离、保护和地址空间独立。扩大可用内存只是虚拟内存体系中更后续的一部分主题。

14.2 页表不是保存“每个虚拟地址到物理地址”

分页以页为单位管理内存。页表保存的是“虚拟页号 → 物理页号”,页内偏移直接保留。

14.3 知道别人的虚拟地址没有直接意义

虚拟地址必须绑定进程地址空间。进程 A 知道进程 B 的虚拟地址,并不代表 A 能让 MMU 使用 B 的页表。

14.4 多级页表不是 MMU 创建的

多级页表由操作系统创建和维护。MMU 的职责是读取页表并执行地址翻译。

14.5 TLB 不是另一张完整页表

TLB 是缓存,不是权威数据结构。TLB 未命中时仍需要查询页表。


❗ 本节核心结论: 理解虚拟内存的关键,是把“地址空间上下文、页表映射、硬件翻译、缓存加速”四件事分清楚。

15. 复习清单

学完《虚拟内存管理 I》后,应能回答以下问题:

  1. 为什么应用程序不应该直接使用物理地址?
  2. 虚拟地址和物理地址分别是什么?
  3. 为什么虚拟地址必须绑定进程地址空间?
  4. 分段机制的地址结构是什么?
  5. 分段为什么会产生外部碎片?
  6. 分页机制下,虚拟地址由哪两部分组成?
  7. 页表保存的到底是什么映射?
  8. 为什么页内偏移在地址翻译后保持不变?
  9. 单级页表大小如何计算?
  10. 为什么 64 位系统不能简单使用完整单级页表?
  11. 多级页表为什么能节省空间?
  12. AArch64 四级页表中 9 + 9 + 9 + 9 + 12 分别表示什么?
  13. 页表项除了物理页号,还可能保存哪些控制信息?
  14. MMU 在地址翻译中负责什么?
  15. 操作系统在页表管理中负责什么?
  16. TLB 缓存的是什么?
  17. 为什么程序局部性会提高 TLB 的有效性?
  18. ASID 为什么能减少进程切换时的 TLB 刷新开销?

结论

《虚拟内存管理 I》的核心链条可以压缩为一句话:

程序使用虚拟地址;操作系统为每个进程建立独立的地址翻译规则;MMU 根据当前规则把虚拟地址翻译为物理地址;多级页表节省页表空间;TLB 缓存翻译结果以降低查表成本。

因此,虚拟内存不是单个机制,而是一组机制的组合。分段和分页解决地址组织问题,页表解决映射保存问题,多级页表解决空间开销问题,TLB 解决翻译速度问题,ASID 解决多进程上下文切换中的缓存混淆问题。

真正掌握这一节,关键不在于死记每个字段,而在于形成一个稳定直觉:程序运行在虚拟地址世界中,物理内存由操作系统和硬件共同隐藏、翻译和保护。