第5讲 虚拟内存管理I

第5讲 虚拟内存管理I
agsd虚拟内存管理 I:从地址隔离到页表翻译
虚拟内存管理的第一层问题,并不是“物理内存不够时怎么办”,而是“程序为什么不能直接使用物理地址”。现代操作系统让每个进程运行在自己的虚拟地址空间中,程序看到的是虚拟地址,真正访问内存时再由硬件和操作系统配合,把虚拟地址翻译成物理地址。
这套机制的核心由四个部分组成:
- 操作系统:建立和维护地址翻译规则。
- 页表或段表:保存虚拟地址空间到物理内存的映射关系。
- MMU:在 CPU 访存时执行地址翻译。
- TLB:缓存近期翻译结果,降低查页表的成本。
❗ 核心结论: 虚拟内存不是单纯为了“扩大内存”,而是为了给每个进程提供独立、受保护、可管理的地址空间。
1. 为什么不能直接使用物理地址
物理内存就是机器上真实存在的内存条。程序代码、程序数据、操作系统代码和操作系统数据,最终都必须位于物理内存中,CPU 才能访问。
在早期单程序环境中,物理内存可以简单划分为:
1 | 物理内存 |
但在多程序系统中,多个应用会同时驻留在内存中:
1 | 物理内存 |
如果应用程序直接使用物理地址,会带来几个问题:
- 缺乏隔离:一个程序可能访问或破坏另一个程序的数据。
- 加载位置不稳定:程序被加载到哪里取决于当时物理内存的空闲情况。
- 暴露真实内存布局:程序能够感知甚至推断系统和其他程序的位置。
- 管理困难:操作系统难以灵活移动、共享或保护内存区域。
因此,虚拟内存首先解决的是地址抽象和隔离问题。程序使用虚拟地址,物理地址由系统在背后管理。
❗ 本节核心结论: 程序不应直接使用物理地址,因为物理地址暴露真实内存布局,并削弱进程隔离与系统管理能力。
2. 虚拟地址与地址翻译
虚拟内存抽象下,程序中看到的地址通常不是物理地址。例如程序打印出的指针值,本质上属于当前进程的虚拟地址空间。
一次普通内存访问可以概括为:
flowchart LR
CPU[CPU 发出虚拟地址] --> MMU[MMU 地址翻译]
MMU --> PA[得到物理地址]
PA --> MEM[访问物理内存]
这张图强调:程序并不是直接拿物理地址访问内存,而是先经过 MMU 的地址翻译。
各组件的职责可以概括为:
| 组件 | 主要职责 |
|---|---|
| CPU | 执行指令,并发出虚拟地址 |
| MMU | 根据当前地址翻译规则,把虚拟地址翻译为物理地址 |
| 操作系统 | 创建、维护、切换段表或页表 |
| 页表/TLB | 页表保存映射关系,TLB 缓存近期翻译结果 |
这里最重要的边界是:操作系统负责建表,MMU 负责查表。 MMU 并不会凭空创造映射关系,它只是使用当前进程对应的翻译规则。
❗ 本节核心结论: 地址翻译是硬件与操作系统合作完成的:操作系统建立规则,MMU 在访存路径上使用规则。
3. 虚拟地址不是全局地址
一个常见误区是:如果程序 A 知道程序 B 的虚拟地址,是否就能拿这个地址去查 B 的段表或页表,从而得到 B 的物理地址?
答案是:正常情况下不能。
原因在于虚拟地址不是全局地址。虚拟地址必须绑定到“当前是哪一个进程、当前使用哪一张地址翻译表”这个上下文中才有意义。
例如,同样的虚拟地址在两个进程中可能对应不同物理地址:
1 | 进程 A:虚拟地址 0x400000 → 物理地址 X |
当进程 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 | 虚拟页号 = 0x12 |
如果页表中有映射:
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 | 虚拟页号 → 页表项 |
多级页表更像一棵按需展开的树:
flowchart TD
L0[L0 页表] --> L1a[L1 页表]
L0 --> Hole1[空洞:不分配下级页表]
L1a --> L2a[L2 页表]
L1a --> Hole2[空洞:不分配下级页表]
L2a --> L3a[L3 页表]
L3a --> P[物理页]
这张图说明:如果某个地址区域没有被使用,对应的下级页表可以不存在,从而节省空间。
多级页表的代价是翻译变慢。如果没有 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 | 虚拟页号_0 | 虚拟页号_1 | 虚拟页号_2 | 虚拟页号_3 | 页内偏移 |
每一级 9 位,可以索引:
1 | 2^9 = 512 |
也就是说,每一级页表最多有 512 个页表项。
AArch64 四级页表的翻译过程可以概括为:
flowchart TD
VA[虚拟地址] --> Split[拆分为 9+9+9+9+12]
Split --> TTBR[TTBR0_EL1 指向 L0 页表]
TTBR --> L0[用第 1 个 9 位索引 L0]
L0 --> L1[用第 2 个 9 位索引 L1]
L1 --> L2[用第 3 个 9 位索引 L2]
L2 --> L3[用第 4 个 9 位索引 L3]
L3 --> PPN[得到物理页号]
PPN --> PA[物理页号 + 页内偏移 = 物理地址]
这张图强调:四级页表并不是四种不同地址,而是把虚拟页号分成四段,逐层缩小搜索范围,最后找到物理页号。
❗ 本节核心结论: 在 4KB 页、48 位有效虚拟地址下,AArch64 四级页表的地址拆分规律是 9 + 9 + 9 + 9 + 12。
9. 页表项不仅保存物理页号
页表项的核心内容是物理页号,但它通常不只保存物理页号。它还包含权限和状态信息。
常见字段包括:
| 字段 | 作用 |
|---|---|
| valid 位 | 表示页表项是否有效 |
| AP 位 | 控制读写权限 |
| XN 位 | 控制用户态是否禁止执行 |
| PXN 位 | 控制内核态是否禁止执行 |
| AF 位 | Access Flag,可用于记录页面是否被访问 |
| AttrIndx | 指示内存类型,如普通内存或设备内存 |
因此,页表不只是“地址翻译表”,也是保护机制的一部分。MMU 在查页表时,不仅要得到物理页号,还可能检查当前访问是否合法。
当页表项无效、权限不足或执行控制不允许时,MMU 不应简单返回物理地址,而应触发异常,让操作系统处理。
❗ 本节核心结论: 页表项既表达地址映射,也表达访问权限、执行控制和内存属性;页表同时承担翻译与保护功能。
10. 页表使能:从物理寻址到虚拟寻址
CPU 刚启动时,系统通常处于较原始的物理寻址阶段。操作系统或底层系统软件初始化页表后,会配置控制寄存器,使地址翻译生效。
典型例子包括:
| 架构 | 控制方式 |
|---|---|
| AArch64 | 设置 SCTLR_EL1 的 M 位 |
| x86_64 | 设置 CR0 的 PG 位 |
可以简化理解为:
1 | 页表未使能:CPU 发出的地址直接当作物理地址 |
这说明虚拟内存不是只存在于软件概念中,它需要硬件控制位和页表基地址寄存器共同支持。
❗ 本节核心结论: 页表只有在硬件控制位开启后才参与访存路径;开启后,CPU 发出的地址会经过 MMU 翻译。
11. TLB:让多级页表重新变快
多级页表节省空间,但也增加了地址翻译成本。若每次访问内存都要查四级页表,那么一次普通访存可能变成:
1 | 查 L0 页表 |
为降低这个成本,CPU 内部提供 TLB:
1 | TLB = Translation Lookaside Buffer |
它缓存的是近期使用过的地址翻译结果:
1 | 虚拟页号 → 物理页号 |
地址翻译过程可以概括为:
flowchart TD
VA[虚拟地址] --> TLB{TLB 命中?}
TLB -- 是 --> PPN[直接得到物理页号]
TLB -- 否 --> WALK[执行多级页表查询]
WALK --> FILL[把结果填入 TLB]
FILL --> PPN
PPN --> PA[拼接页内偏移]
TLB 有效的原因是程序访问内存具有局部性:
| 局部性 | 含义 | 例子 |
|---|---|---|
| 时间局部性 | 刚访问过的地址,之后很可能再次访问 | 循环中反复访问同一变量 |
| 空间局部性 | 访问某地址后,附近地址也很可能被访问 | 顺序遍历数组 |
例如连续访问数组 a[i] 时,多个元素可能位于同一页内。只要这一页的映射已经在 TLB 中,访问同页其他元素时就可以复用同一条翻译结果。
❗ 本节核心结论: TLB 是页表查询结果的缓存,它利用程序访问局部性减少多级页表查询次数。
12. 进程切换、TLB 刷新与 ASID
TLB 缓存的是虚拟页号到物理页号的映射。但不同进程有不同页表,可能出现:
1 | 进程 A:虚拟页 0 → 物理页 1 |
如果进程切换后仍错误复用旧进程的 TLB 项,就会把 A 的映射用于 B,造成严重错误。
最直接的解决方式是:每次进程切换都刷新 TLB。但这会丢失缓存,性能较差。
更好的办法是给 TLB 项加入地址空间标签。例如 AArch64 使用 ASID:
1 | ASID = Address Space ID |
TLB 项可理解为:
1 | ASID + 虚拟页号 → 物理页号 |
这样,即使两个进程都有虚拟页 0,也能区分:
1 | ASID 1 + 虚拟页 0 → 物理页 1 |
ASID 的意义是:把“当前属于哪个地址空间”这个上下文显式加入 TLB 项,从而减少不必要的全量刷新。
❗ 本节核心结论: ASID 让 TLB 项带上地址空间身份,避免不同进程的相同虚拟页号发生混淆。
13. MMU、操作系统、页表和 TLB 的职责边界
虚拟内存管理容易混淆的地方,是把“谁建立规则”和“谁使用规则”混为一谈。可以用下表区分:
| 角色 | 做什么 | 不做什么 |
|---|---|---|
| 操作系统 | 创建页表、分配页表页、填写 PTE、设置权限、切换页表 | 不在每次普通访存时手动翻译地址 |
| MMU | 根据当前页表和 TLB 执行地址翻译与权限检查 | 不自行决定哪些虚拟页应映射到哪些物理页 |
| TLB | 缓存近期翻译结果 | 不替代页表作为权威映射来源 |
| CPU | 执行指令并发起访存 | 不直接理解进程隔离策略 |
多级页表的创建流程可以简化为:
flowchart TD
Need[需要映射某个虚拟地址] --> Check0[检查 L0 项]
Check0 -->|不存在| Alloc1[分配 L1 页表]
Check0 -->|存在| Check1[检查 L1 项]
Alloc1 --> Check1
Check1 -->|不存在| Alloc2[分配 L2 页表]
Check1 -->|存在| Check2[检查 L2 项]
Alloc2 --> Check2
Check2 -->|不存在| Alloc3[分配 L3 页表]
Check2 -->|存在| Fill[填写最终页表项]
Alloc3 --> Fill
Fill --> Done[写入物理页号与权限位]
这张图说明:多级页表由操作系统按需创建;程序运行时,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》后,应能回答以下问题:
- 为什么应用程序不应该直接使用物理地址?
- 虚拟地址和物理地址分别是什么?
- 为什么虚拟地址必须绑定进程地址空间?
- 分段机制的地址结构是什么?
- 分段为什么会产生外部碎片?
- 分页机制下,虚拟地址由哪两部分组成?
- 页表保存的到底是什么映射?
- 为什么页内偏移在地址翻译后保持不变?
- 单级页表大小如何计算?
- 为什么 64 位系统不能简单使用完整单级页表?
- 多级页表为什么能节省空间?
- AArch64 四级页表中
9 + 9 + 9 + 9 + 12分别表示什么? - 页表项除了物理页号,还可能保存哪些控制信息?
- MMU 在地址翻译中负责什么?
- 操作系统在页表管理中负责什么?
- TLB 缓存的是什么?
- 为什么程序局部性会提高 TLB 的有效性?
- ASID 为什么能减少进程切换时的 TLB 刷新开销?
结论
《虚拟内存管理 I》的核心链条可以压缩为一句话:
程序使用虚拟地址;操作系统为每个进程建立独立的地址翻译规则;MMU 根据当前规则把虚拟地址翻译为物理地址;多级页表节省页表空间;TLB 缓存翻译结果以降低查表成本。
因此,虚拟内存不是单个机制,而是一组机制的组合。分段和分页解决地址组织问题,页表解决映射保存问题,多级页表解决空间开销问题,TLB 解决翻译速度问题,ASID 解决多进程上下文切换中的缓存混淆问题。
真正掌握这一节,关键不在于死记每个字段,而在于形成一个稳定直觉:程序运行在虚拟地址世界中,物理内存由操作系统和硬件共同隐藏、翻译和保护。



