Lecture 13–14: 虚拟内存(Virtual Memory)
Lecture 13–14: 虚拟内存(Virtual Memory)
概述
本讲把虚拟化思想应用到内存:让多个进程共享同一物理内存,同时获得多任务、透明、隔离与效率。我们沿历史演进:单任务 → 加载时重定位 → 基址/界限(base and bound)(第一个硬件 MMU)→ 分段(segmentation),理解每步解决什么问题、又留下什么局限,为下一讲的分页(paging)铺路。
核心概念与系统机制图解
内存共享的目标(Lecture 13 的评分表)
| 目标 | 含义 | | :— | :— | | 多任务(Multitasking) | 多个进程可同时驻留内存 | | 透明(Transparency) | 进程察觉不到共享——每个进程都像独占内存 | | 隔离(Isolation) | 进程不能破坏彼此或 OS | | 效率(Efficiency) | 共享不严重损害 CPU 与内存效率 |
演进路线与评分
单任务(批处理/MS-DOS) 加载时重定位 基址/界限(MMU)
OS 独占高地址, 一次一程序 加载器像链接器一样改地址 硬件寄存器: base + bound
多任务 ✗ 透明 ✗ 隔离 ✗ 多任务 ✓ 透明 ✗ 隔离 ✗ 多任务 ✓ 透明 ✓ 隔离 ✓
效率 ✓ 效率 ✓(但碎片/无法增长) 效率 ✓
局限: 不能共享, 坏程序 局限: 尺寸静态声明、 局限: 每进程一个连续区域,
能破坏 OS 无隔离、碎片化、不能移动 不支持共享/只读代码、碎片化
动态地址翻译(Dynamic Address Translation)
- 核心思想:程序看到的是虚拟地址空间(virtual address space),硬件 MMU 在每次内存访问时把它翻译成物理地址空间(physical address space)中的地址。
CPU ──虚拟地址──► MMU ──物理地址──► 内存 │ └─(同时检查越界/保护) - 每个进程有独立的虚拟地址空间(都从 0 开始);物理地址空间被 OS 划分给各进程。
基址/界限(Base and Bound)MMU
两个硬件寄存器:
Base = 进程在物理内存中的起始位置
Bound = 进程虚拟地址空间的大小
每次访问(并行完成):
物理地址 = 虚拟地址 + Base
若 虚拟地址 >= Bound → 越界故障(fault)
示例:虚拟地址 140,Base=6000 → 物理地址 6140;程序里所有地址(PC、SP、数据引用)都经 MMU 加上 Base,程序本身无需重定位——这就是”动态”重定位。
进程 ⇔ OS 切换时的地址翻译
- OS 运行时关闭地址翻译(虚拟地址 = 物理地址),通过处理器状态寄存器(PSR)的位控制”翻译开/关、用户/内核模式”。
- 陷入(trap)OS 时原子地:保存程序计数器 → 跳到中断向量(interrupt vector)指向的 OS 入口 → 关闭翻译与用户模式。
- 从 trap 返回时原子地:恢复翻译与用户模式 → 恢复保存的 PC。
分段(Segmentation):多个区域
- 动机:一个连续区域不够用——程序由代码、数据、栈等多段组成,需要分别管理(分别增长、交换、共享、保护)。
- 机制:段表(segment map),每段有 base、bound、保护位(如代码段只读)。
段表(每进程): 段号 类型 Base Bound 保护 0 Code 1000 1000 R/O 1 Data 3000 2000 R/W 2 Stack 8000 2000 R/W 地址高位选段, 低位是段内偏移; 或由指令类型隐式选段(x86 段前缀) - 优点:各段独立增长/交换、可移动压缩消除碎片、可共享(共享代码段)。
- 缺点:段数量固定仍有限制(无法 mmap 文件);变长段仍有碎片;地址空间划分僵化。
代码示例与系统调用解说
示例:查看进程的虚拟内存布局(Linux /proc)
# 编译并运行一个简单程序
./myprogram &
# 查看其虚拟地址空间布局
cat /proc/$!/maps
# 输出示例 (每行: 虚拟地址范围 权限 偏移 设备 inode 路径)
# 00400000-00401000 r-xp 00000000 08:01 12345 /home/user/myprogram ← 代码段(只读可执行)
# 00600000-00601000 r--p 00000000 08:01 12345 /home/user/myprogram ← 数据段(只读)
# 00601000-00602000 rw-p 00001000 08:01 12345 /home/user/myprogram ← 数据段(可写)
# 7ffc00000000-7ffc00021000 rw-p 00000000 00:00 0 [stack] ← 栈
# 7f0000000000-7f0000020000 r-xp ... /lib/x86_64-linux-gnu/libc.so.6 ← 共享库代码
# ... [heap] ...
【代码做了什么?】 /proc/<pid>/maps 打印进程虚拟地址空间中每个映射(代码、数据、堆、栈、共享库)的虚拟地址范围与权限。
【系统机制透视】
- 这就是”虚拟地址空间”的真实面貌:进程”以为”自己独占从 0x0 到 0x7fffffffffff 的地址空间,但物理内存中这些页可以散布在任何位置(分页,下一讲)。
- 权限位(r-x、rw-p)就是 MMU 执行保护检查的依据:试图写入只读页会触发段错误(segmentation fault)——隔离与保护正是这样硬件化的。
- 共享库出现在每个进程同一虚拟地址(如 libc),物理上只有一份——分段/分页的共享机制。
关键要点
- 虚拟化内存 = 让每个进程拥有从 0 开始的私有虚拟地址空间,MMU 动态翻译为物理地址。
- 基址/界限是第一个简单 MMU:一次加法和一次比较,同时实现重定位、隔离、透明。
- 基址/界限的局限(一个连续区域)催生分段:多段独立管理、可共享、可保护。
- 分段的局限(固定段数、碎片)催生分页:固定大小页,无外部碎片。
- trap 进出 OS 时原子地切换”翻译开关 + 用户/内核模式”。
常见陷阱与注意事项
- 混淆虚拟地址与物理地址:程序里所有指针都是虚拟地址。
- 以为进程能直接访问物理内存:用户进程永远看不到物理地址(除 OS 的特殊映射)。
- 越界与保护:基址/界限和段表都检查越界;忘记保护位(如代码段可写)会破坏隔离。
- 把”重定位”理解成加载期一次性完成:base-and-bound 是每次访问动态完成,程序无需修改。
思考题
- 问题:基址/界限机制中,为什么程序无需修改任何地址就能在任意位置运行?
- 答案:MMU 在每次内存访问时把虚拟地址加上基址寄存器,程序看到的是从 0 开始的连续虚拟空间;物理位置由基址决定,与程序内容无关——”动态重定位”。
- 问题:分段相比基址/界限解决了什么问题,又引入了什么新问题?
- 答案:解决”单一连续区域”限制——各段独立增长/交换/共享/保护;引入”段数固定、变长段的外部碎片、地址空间划分僵化”等新问题。
- 问题:OS 运行时为什么关闭地址翻译?
- 答案:OS 代码与数据结构直接操作物理内存(早期设计),关闭翻译后虚拟地址=物理地址,便于内核管理物理资源;进出 trap 时切换 PSR 位实现。
