内存管理的基本原理和要求
第 3 章的全部内容,都是在回答一个问题:程序里写的地址,和它在内存条上实际待的位置,怎么对上。
这一节先把这条链条的起点讲清——一段源代码要经过哪些加工才能跑起来,地址是在哪一步被确定的。后面几节(连续分配、分页、分段)不过是给出了不同的对应方案。
机制
从源代码到运行:三步加工
疑问点:编译、链接、装入三阶段各自完成什么
教材原文:“编译:由编译程序将用户源代码编译成若干目标模块。链接:由链接程序将编译后形成的一组目标模块,以及它们所需的库函数链接在一起,形成一个完整的装入模块。装入:由装入程序将装入模块装入内存运行。“(2011 统考考点)
三步的产物依次是目标模块 → 装入模块 → 内存中的进程:
| 阶段 | 谁来做 | 输入 | 输出 |
|---|---|---|---|
| 编译 | 编译程序 | 源代码 | 若干目标模块(各自独立) |
| 链接 | 链接程序 | 一组目标模块 + 库函数 | 一个完整的装入模块 |
| 装入 | 装入程序 | 装入模块 | 内存中可运行的映像 |
要抓住的关键是**“若干”和”一个”的对比**:编译产出的是多个互不相识的目标模块,各自内部的地址都从 0 开始编号;链接的职责就是把它们拼成一个统一编址的整体,并把彼此的符号引用(你调用了我的函数)解析掉。
三种链接方式
疑问点:三种链接方式的区别
静态链接、装入时动态链接、运行时动态链接分别在何时完成,各有什么代价?
判据只有一条:库代码是在什么时候被拼进来的。
静态链接——运行之前就把所有目标模块和库函数全部链接成一个完整的装入模块,此后不再拆开。
优点是运行时不需要再找库,速度快、部署简单。缺点是每个可执行文件都自带一份库的副本,磁盘和内存都浪费;库一旦升级,所有程序都得重新链接。
装入时动态链接——目标模块边装入边链接:装入程序发现需要某个模块时,就去找到它、装入内存、接上。
优点是便于修改和更新(换掉某个模块即可),也便于实现模块共享。
运行时动态链接——真正用到某个模块时才装入并链接。
这是三者中最灵活的:程序中很多分支可能根本不会执行(错误处理、罕见功能),它们对应的模块就永远不必被装入。既加快了装入速度,又节省了内存。
关联对照:安装包体积与链接方式
“同样的软件 macOS 的包比 Windows 大”这个观察,与本节内容确实相关,而且能从两个角度解释。
第一个角度就是链接方式。 静态链接把依赖的库整个打进可执行文件,包自然大;动态链接只留一个引用,运行时才去加载系统里的共享库。分发时若不能假定目标机器上有某个库,就只能静态链接或把库一起打包。
第二个角度与本节无关,但它才是 macOS 包偏大的主因:macOS 的
.app是一个目录包(bundle),里面除可执行文件外还塞了图标、多语言资源、Info.plist等;更关键的是通用二进制(universal binary)——一个可执行文件里同时包含 x86_64 和 arm64 两套完整机器码,体积直接翻倍。这一条属于工程常识而非考纲内容,此处列出是为了把”打包格式”与”链接/装入”两件事分开:打包决定磁盘上的文件长什么样,链接与装入决定它如何变成内存中的进程。
三种装入方式与重定位
装入要解决的核心问题是:装入模块里的地址是从 0 开始的相对地址(逻辑地址),而它实际会被放到内存的某个位置(物理地址),这个差值怎么补上。 补这个差值的动作就叫重定位。
疑问点:三种装入方式的适用范围
教材原文:“(1)绝对装入:绝对装入方式只适用于单道程序环境。在编译时,若知道程序将放到内存的哪个位置,则编译程序将产生绝对地址的目标代码……”
① 绝对装入——编译时就直接产生物理地址,装入时原样放进去,不需要任何重定位。
前提是编译时就知道程序会被放在哪儿。这只在单道程序环境下成立——内存里只有一道程序,位置固定。多道环境下谁也不知道自己会被放到哪,这个前提立刻崩塌。
② 可重定位装入(静态重定位)——装入时一次性把装入模块中的所有相对地址改成绝对地址。
装入之后,程序里存的就是真实的物理地址了。代价是程序一旦装入就不能再移动——因为地址已经被写死,移动了就全错了。而且它要求在装入时就分配到全部所需内存。
③ 动态运行时装入(动态重定位)——装入时不做任何地址修改,程序中保留的仍是逻辑地址。每次访问内存时,由硬件把逻辑地址加上重定位寄存器的值,实时算出物理地址。
因为地址转换推迟到了每次访存的那一刻,程序在内存中可以随时被移动——移动后只需改一下重定位寄存器即可。这是挂起、紧凑、虚拟存储等一切高级功能的基础。
三者的分界点非常清楚:地址是在什么时候被固定下来的——编译时(绝对装入)、装入时(静态重定位)、还是访存时(动态重定位)。
动态重定位依赖什么
疑问点:动态重定位过程所依赖的部件
- 动态重定位的过程依赖于( )。 I. 可重定位装入程序 II. 重定位寄存器 III. 地址变换机构 IV. 目标程序
答案是 I、II、III。
逐项落到具体动作上,就很清楚了:
- I 可重定位装入程序——负责把装入模块整体搬进内存,并把它的起始地址写入重定位寄存器。没有这一步,后面的转换无从谈起。
- II 重定位寄存器——存放程序在内存中的起始物理地址,是转换公式里的那个加数。
- III 地址变换机构——硬件部件,每次访存时执行
物理地址 = 逻辑地址 + 重定位寄存器。
IV 目标程序为什么不需要:目标程序是编译阶段的产物,在链接阶段就已经被消耗掉、合成为装入模块了。到了装入和运行阶段,系统面对的是装入模块,早已不需要原始的目标模块。
顺带澄清一个流传的错误解释:有的解析说”可重定位装入程序在重定位的过程中执行”——这个说法是错的。可重定位装入程序只在装入那一刻执行一次,之后就退出了;运行期间每次访存的地址转换是由硬件的地址变换机构完成的,与装入程序无关。
选 I 的正确理由是它负责装入并设置重定位寄存器,而不是”它在重定位时执行”。
内存保护
多道程序共享内存,必须防止一个进程访问到别人的空间。两种方法:
① 上、下限寄存器——存放该进程内存空间的上界和下界。每次访存时硬件检查地址是否落在区间内,越界则产生越界中断。
② 重定位寄存器 + 界地址寄存器——重定位寄存器存起始物理地址(基址),界地址寄存器存逻辑地址的最大值(限长)。
检查过程分两步:先判断 逻辑地址 < 界地址寄存器 是否成立,不成立则越界;成立才执行 物理地址 = 逻辑地址 + 重定位寄存器。
注意顺序:先查界,后加基。 反过来做的话,一个越界的逻辑地址加上基址后可能”恰好”落进别人的合法区域,检查就形同虚设。
边界
逻辑地址 vs 物理地址
| 逻辑地址(相对地址) | 物理地址(绝对地址) | |
|---|---|---|
| 谁看到 | 用户程序、编译器 | 内存单元的真实编号 |
| 起点 | 每个进程都从 0 开始 | 全内存统一编址 |
| 谁来转换 | — | 地址变换机构(硬件) |
用户程序只能看到逻辑地址,这是内存保护的前提——它连别人的物理地址是多少都不知道,自然也就无从访问。
静态重定位 vs 动态重定位
这是本节最高频的辨析,判据是地址什么时候被改:
| 静态重定位 | 动态重定位 | |
|---|---|---|
| 何时转换 | 装入时,一次性改完 | 每次访存时,实时算 |
| 程序中存的是 | 转换后的物理地址 | 仍是逻辑地址 |
| 需要硬件支持 | 不需要 | 需要(重定位寄存器 + 地址变换机构) |
| 装入后能否移动 | 不能 | 可以(只需改寄存器) |
| 内存要求 | 必须一次分配全部所需空间 | 可分批、可不连续 |
哪些管理方式需要地址变换机构
疑问点:固定分区分配是否需要地址变换机构
各种存储分配方式中,哪些需要地址变换机构?固定分区分配需要吗,为什么?
固定分区分配不需要(可以不需要),原因是它可以采用静态重定位。
推理链是这样的:能否用静态重定位,取决于”程序装入之后会不会被移动”。
- 单一连续分配、固定分区分配:分区的位置和大小在系统初始化时就定死了,作业装入某个分区后就一直待在那儿,绝不移动。既然位置永久不变,装入时把地址一次性改成物理地址就完全可行——用静态重定位即可,不需要地址变换机构。
- 可变分区(动态分区)分配:分区是按需划出来的,而且系统会做紧凑(把零散的空闲区拼到一起),这就要移动已经装入的作业。地址一旦写死就没法移动了,因此必须用动态重定位,需要地址变换机构。
- 分页、分段、段页式:逻辑地址被拆成”页号+页内偏移”这类结构,必须查页表/段表才能算出物理地址,天然需要地址变换机构(且通常还需要 MMU 硬件)。
一句话:只有位置永久固定的方案才能省掉地址变换机构。
对应到那道选择题——“下面的存储管理方案中,哪种可以采用静态重定位”,答案是固定分区。
可变分区是什么
这是个术语别名问题:可变分区 = 动态分区,指分区的大小和数目不是预先划定的,而是在装入作业时按其实际需要临时划出来。详见 3.1.2。
与之相对的固定分区是把内存预先划成若干个固定大小的分区,分区数目和边界在运行期间不变。
可重入程序与”对换”
疑问点:可重入程序改善系统性能的机制
- 可重入程序是通过( )方法来改善系统性能的。 A. 改变时间片长度 B. 改变用户数 C. 提高对换速度 D. 减少对换数量
其中的”对换”指什么?
答案 D:减少对换数量。
可重入程序(纯代码,pure code)指在执行过程中不修改自身的代码。因为它不会被改写,所以多个进程可以共享内存中的同一份副本——每个进程只需要各自保存一份私有的数据区。
这里的**“对换”(swapping)** 指的是把整个进程在内存与外存之间换进换出——内存不够时把某些进程整体写到外存的对换区,需要时再读回来。这是中级调度所做的事。
因果链是:可重入程序被共享 → 内存中不必存放多份相同代码 → 内存占用下降 → 需要被换出到外存的进程变少 → 对换次数减少。
排除其他选项也很直接:可重入与时间片长度(A)、用户数(B)都无关;它也不改变对换本身的速度(C),而是从源头减少了对换的数量。
对照速查
| 链接方式 | 何时链接 | 特点 |
|---|---|---|
| 静态链接 | 运行前全部链接完 | 简单快,但每份程序自带库副本 |
| 装入时动态链接 | 边装入边链接 | 便于更新和共享模块 |
| 运行时动态链接 | 用到时才链接 | 最省内存,装入最快 |
| 装入方式 | 地址何时固定 | 需硬件 | 装入后可移动 | 适用 |
|---|---|---|---|---|
| 绝对装入 | 编译时 | 否 | 否 | 仅单道程序 |
| 可重定位装入(静态重定位) | 装入时 | 否 | 否 | 固定分区 |
| 动态运行时装入(动态重定位) | 每次访存时 | 是 | 是 | 可变分区、分页分段等 |
| 管理方式 | 可用静态重定位 | 需地址变换机构 |
|---|---|---|
| 单一连续、固定分区 | 是 | 否 |
| 可变分区(动态分区) | 否(要紧凑) | 是 |
| 分页 / 分段 / 段页式 | 否 | 是 |
考点
- 编译 → 链接 → 装入 三阶段的产物:若干目标模块 → 一个装入模块 → 内存映像
- 三种链接方式的时机,运行时动态链接最省内存
- 三种装入方式:地址在编译时 / 装入时 / 访存时被固定
- 绝对装入只适用于单道程序环境
- 动态重定位依赖 I 装入程序、II 重定位寄存器、III 地址变换机构,不依赖目标程序
- 固定分区可用静态重定位,故不需要地址变换机构
- 内存保护:先查界地址,后加重定位寄存器,顺序不可颠倒
- 可重入程序通过被共享而减少对换数量
链接
- 🏠 返回总览:操作系统第 3 章:内存管理总览
- ➡️ 下一节:3.1.2 连续分配管理方式
- 🔗 对换与挂起态见 2.1.3 进程的状态与转换
- 📖 名词库:第 3 章名词库