Memory Management Overview
存储管理
多个程序如何同时装入内存?如何互不干扰?如何让程序看到一套稳定的地址空间?
存储管理主要处理四件事:
| 功能 | 要解决的问题 |
|---|---|
| 内存空间的分配与回收 | 哪个进程占用哪段内存,进程结束后如何释放 |
| 地址转换 | 程序中的逻辑地址如何变成真正访问主存的物理地址 |
| 内存保护 | 一个进程不能越界访问其他进程或操作系统的内存 |
| 内存共享 | 多个进程如何共享同一段代码或同一份数据 |
| 内存扩展 | 物理内存不够时,如何让程序仍然能运行 |
程序如何进入内存
C程序从源码到可执行文件,要经过预处理、编译、汇编、链接四个阶段;可执行文件还必须被装入内存,CPU 才能执行。
存储管理更关心后半段:
- 编译形成目标模块。
- 链接把目标模块和库函数合成装入模块。
- 装入程序把装入模块放入内存。
- CPU 执行指令时,把指令中的逻辑地址转换为物理地址。
编译、链接、装入
| 阶段 | 结果 | 说明 |
|---|---|---|
| 编译 | 目标模块 | 把高级语言翻译成机器语言形式的模块 |
| 链接 | 装入模块 | 把多个目标模块和库函数连接成完整程序 |
| 装入 | 内存中的进程 | 把装入模块放入主存,使其可以运行 |
源码中的变量名、函数名不会直接出现在 CPU 的访存过程中。经过编译和链接后,指令里使用的是某种地址;操作系统和硬件要保证这些地址最终能定位到正确的物理内存单元。
链接
链接方式的核心区别是:多个目标模块和库函数在什么时候合成完整程序。
| 链接方式 | 时机 | 特点 |
|---|---|---|
| 静态链接 | 程序运行前 | 先把目标模块和库函数链接成完整装入模块 |
| 装入时动态链接 | 装入内存时 | 边装入边链接 |
| 运行时动态链接 | 程序执行中需要时 | 用到某个模块时才链接,便于共享、更新和节省内存 |
运行时动态链接是现代系统中共享库思想的重要基础。一个程序并不一定要把所有可能用到的模块都提前装入内存。
地址空间
程序经过编译、链接后生成的指令通常使用的是逻辑地址,也称相对地址。它相对于进程自己的起始位置而言,而不是在主存中的绝对位置。
| 地址 | 含义 |
|---|---|
| 逻辑地址 | 程序看到的地址,通常从 0 开始 |
| 物理地址 | 主存中的真实地址 |
| 地址转换 | 把逻辑地址变成物理地址的过程,也称地址重定位 |
如果一个程序的逻辑地址 79 对应变量 x,而该进程被装入到物理地址 100 开始的位置,那么实际访问的物理地址应是:
$$
100 + 79 = 179
$$
装入
装入方式的核心区别是:逻辑地址在什么时候变成物理地址。
| 装入方式 | 地址转换时机 | 特点 |
|---|---|---|
| 绝对装入 | 编译或汇编时已经产生绝对地址 | 只适合很简单的单道程序环境,逻辑地址就是实际物理地址 |
| 可重定位装入 | 装入时一次性把逻辑地址改成物理地址 | 又称静态重定位;装入后不能随意移动 |
| 动态运行时装入 | 真正执行访问时才转换地址 | 又称动态重定位;需要重定位寄存器支持 |
绝对装入
绝对装入要求事先知道程序会放到主存的哪个位置,目标代码中直接使用绝对地址。装入程序只需按这些地址把代码和数据放入内存。
这种方式简单,但限制很强:程序必须放在预定位置,难以适应多道程序环境。
静态重定位
静态重定位中,装入模块内部仍从逻辑地址 0 开始。装入时,装入程序根据实际起始物理地址,把所有地址相关内容一次性改成物理地址。
例如装入起始地址为 100,逻辑地址 79 在装入时被改成物理地址 179。
静态重定位的限制是:进程装入时必须一次性分配所需的全部连续空间;一旦装入,在运行期间不能移动,也不能方便地动态扩展。
动态重定位
动态重定位把地址转换推迟到程序执行期间。装入模块进入内存后,指令中仍保留逻辑地址;CPU 执行到访存指令时,由硬件把逻辑地址加上重定位寄存器中的起始物理地址,得到物理地址。
$$
\text{物理地址} = \text{重定位寄存器} + \text{逻辑地址}
$$
动态重定位的优点是灵活:进程可以在运行期间移动,后续的分页、分段、虚拟内存也都依赖“运行时地址转换”这个思想。
内存保护
硬件在地址转换前先检查逻辑地址是否越界。
设某进程的逻辑地址空间为 0~179,实际装入物理地址 100~279:
- 重定位寄存器保存起始物理地址
100。 - 界地址寄存器保存最大合法逻辑地址
179,或保存逻辑地址空间长度。 - CPU 访问逻辑地址
80时,先检查是否越界,再计算物理地址100 + 80 = 180。 - CPU 访问逻辑地址
200时,越过进程边界,触发越界异常。
两种保护方式
| 方式 | 做法 | 说明 |
|---|---|---|
| 上下限寄存器 | 直接保存允许访问的物理地址范围 | CPU 判断目标物理地址是否落在区间内 |
| 重定位寄存器 + 界地址寄存器 | 先检查逻辑地址是否越界,再加基址得到物理地址 | 适合动态重定位 |
内存保护的目标是保证进程只能访问自己的地址空间,不能随意读写其他进程或操作系统内核的数据。
内存分配与回收
内存分配与回收要解决的是:进程装入时放到哪里,运行结束后如何释放它占用的空间。
连续分配方式、空闲分区管理、分配算法和回收合并见 Contiguous-Memory-Allocation。
内存共享
内存共享让多个进程使用同一份物理内存内容,而不是为每个进程都复制一份。
常见共享对象包括:
| 共享对象 | 说明 |
|---|---|
| 共享库代码 | 多个进程调用同一个库函数时,可以共享只读代码段 |
| 可重入代码 | 不会在执行过程中修改自身内容的代码,可以被多个进程安全共享 |
| 共享内存区 | 多个进程有意映射同一块内存,用于进程间通信 |
| 内存映射文件 | 多个进程把同一个文件映射到自己的地址空间,可共享文件内容 |
共享通常要求配合保护机制:只读代码可以共享执行,但不能被普通进程修改;可写共享区则需要同步机制,否则多个进程并发写入会造成数据不一致。
分段管理更容易按逻辑模块表达共享和保护,例如让不同进程的段表项指向同一个只读代码段。分页系统也能共享,但共享单位是页面,表达逻辑模块边界不如分段直观。
内存空间扩展
早期系统中,程序可能大于物理内存,或者内存无法同时容纳太多程序。覆盖技术和交换技术都是在虚拟内存成熟之前用于缓解内存不足的办法。
覆盖技术
覆盖技术把程序划分成若干模块。常用模块常驻内存,不常用模块在需要时调入;不会同时执行的模块可以共用同一个覆盖区。
它能让程序大小超过物理内存,但需要程序员显式设计覆盖结构,对用户不透明,编程负担重,现代系统中基本已成为历史。
交换技术
交换技术以进程为单位进行换入、换出:内存紧张时,把暂时不能运行或优先级较低的进程换出到外存;需要继续运行时,再换入内存。
交换能提高内存利用率,但换入换出涉及外存 I/O,开销较大。虚拟内存和请求分页把这种思想细化到“页”这个粒度。