Lecture 13–14: 虚拟内存(Virtual Memory)

目录 · ← l10 · l12 →

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),物理上只有一份——分段/分页的共享机制。

关键要点

  1. 虚拟化内存 = 让每个进程拥有从 0 开始的私有虚拟地址空间,MMU 动态翻译为物理地址。
  2. 基址/界限是第一个简单 MMU:一次加法和一次比较,同时实现重定位、隔离、透明。
  3. 基址/界限的局限(一个连续区域)催生分段:多段独立管理、可共享、可保护。
  4. 分段的局限(固定段数、碎片)催生分页:固定大小页,无外部碎片。
  5. trap 进出 OS 时原子地切换”翻译开关 + 用户/内核模式”。

常见陷阱与注意事项

  • 混淆虚拟地址与物理地址:程序里所有指针都是虚拟地址。
  • 以为进程能直接访问物理内存:用户进程永远看不到物理地址(除 OS 的特殊映射)。
  • 越界与保护:基址/界限和段表都检查越界;忘记保护位(如代码段可写)会破坏隔离。
  • 把”重定位”理解成加载期一次性完成:base-and-bound 是每次访问动态完成,程序无需修改。

思考题

  1. 问题:基址/界限机制中,为什么程序无需修改任何地址就能在任意位置运行?
    • 答案:MMU 在每次内存访问时把虚拟地址加上基址寄存器,程序看到的是从 0 开始的连续虚拟空间;物理位置由基址决定,与程序内容无关——”动态重定位”。
  2. 问题:分段相比基址/界限解决了什么问题,又引入了什么新问题?
    • 答案:解决”单一连续区域”限制——各段独立增长/交换/共享/保护;引入”段数固定、变长段的外部碎片、地址空间划分僵化”等新问题。
  3. 问题:OS 运行时为什么关闭地址翻译?
    • 答案:OS 代码与数据结构直接操作物理内存(早期设计),关闭翻译后虚拟地址=物理地址,便于内核管理物理资源;进出 trap 时切换 PSR 位实现。