Appearance
虚拟内存基本概念
2026 大纲 三(二)1 虚拟内存的基本概念。
地址变换的问题解决了,但内存还是不够
分页、分段、段页式都在解决同一个问题:进程散着放,地址怎么翻译。 这个问题现在解决得很彻底了——外碎片没了,共享保护也有了, 剩下的开销靠 TLB 和多级页表也压了下来。
可它们全都没碰另一个更硬的限制:进程还是必须整个装进内存才能跑。
这条限制卡死了两件事。单个进程不能比内存大——内存 2 GB, 就跑不了一个需要 3 GB 的程序,无论散着放还是连着放都一样。 并发度也被卡住——每个进程都要全额占着内存,装下几个就只能跑几个。
而与此同时,内存里堆着大量根本没在用的东西:错误处理代码可能一次也不执行, 一个巨大的数组可能只用了开头几百个元素。"必须全部装入"这个要求本身就是浪费的。
于是想法很自然:只把眼下用得着的那部分装进来,用到别的再去取。
但这个想法能不能成立,取决于一件不那么显然的事——程序访问内存的位置, 必须是扎堆的而不是均匀撒开的。如果每条指令都随机访问一个新页面, "用到别的再去取"就意味着几乎每次访存都要读一次磁盘,慢上几万倍, 这套方案当场破产。
好在真实程序确实扎堆,这就是局部性原理。它是一个经验事实,不是逻辑必然—— 虚拟内存的全部可行性建立在它上面。所以这一节从一个反例讲起: 局部性不成立的时候,会有多惨。
一、局部性不成立会怎样:一个反例
推导链的每一环都靠局部性撑着,把这个前提抽掉,结论立刻反向。
假设有一个程序,每次访存都均匀随机落在整个地址空间的任意一页上(例如对一个远大于内存的表做完全随机的散列查找)。设驻留内存的页面只占全部页面的比例
| 情形 | 缺页率 | 相当于内存访问的多少倍 | |
|---|---|---|---|
| 无局部性,驻留 50% | 0.5 | 2 500 050 ns ≈ 2.5 ms | 25 000 倍 |
| 无局部性,驻留 10% | 0.9 | 4 500 010 ns ≈ 4.5 ms | 45 000 倍 |
| 有局部性(典型) | 150 ns | 1.5 倍 |
前两行意味着:没有局部性,虚拟内存会退化成"用磁盘当内存",慢到完全不可用。这也解释了为什么后面所有置换算法(LRU、CLOCK)都在赌局部性,以及为什么一个访问模式反常的程序会把系统拖进抖动。
二、虚拟内存是什么
虚拟内存让每个进程拥有一个很大的、连续的虚拟地址空间,实际上只有一部分页面在物理内存中,其余存放在外存(磁盘)上。
虚拟地址空间内部怎么划分(代码段、数据段、堆向上长、栈向下长、中间留空洞)不属于本节,见运行时内存映像与地址空间。本节只关心一件事:这个空间里的页可以不全在内存。
三个特征为什么是一条链而不是三个并列项(想弄清多次性、对换性、虚拟性谁是手段谁是目的时展开)
| 特征 | 说明 | 是手段还是目的 |
|---|---|---|
| 多次性 | 程序不需要一次全部装入,可以分多次调入内存 | 手段 |
| 对换性 | 暂时不用的部分可以换出到外存,需要时再换入 | 手段 |
| 虚拟性 | 从逻辑上扩充了内存容量,用户感觉内存比实际大得多 | 目的 |
把它们当并列的三条去背,就说不清"为什么必须三条都有"。实际上它们是一条推进链:
多次性解决"进得来"——不必等全部装完就能开始跑;但只有多次性还不够,内存迟早会被填满,装到第
对换性解决"出得去"——把暂时不用的部分换到外存,腾出空间给新的页面,于是"分多次调入"这件事可以无限进行下去,而不是只能进行有限的几轮。
两者合起来,进程实际用到的地址空间就不再受物理内存限制 ⇒ 这才产生了虚拟性。
判据:多次性和对换性是可观察的机制,虚拟性是用户感受到的效果。 所以虚拟性是最本质的特征——去掉它,前两者就没有存在的理由;反过来,缺了前两者中的任何一个,虚拟性都产生不出来。
三、三项硬件支持,按缺口顺序推出来
缺口一:怎么知道某一页在不在内存? ⇒ 页表机制。基本分页的页表项只有"页号 → 页框号",无法表达"这一页不在内存"这种状态,于是扩出四个字段:
| 新增字段 | 补上的缺口 |
|---|---|
| 状态位 P | 回答"在不在内存"——地址变换时先查它 |
| 外存地址 | 回答"不在内存的话,去磁盘的哪个块取" |
| 修改位 M | 回答"换出时要不要写回"(M=0 可直接丢弃,省一次磁盘写) |
| 访问字段 A | 回答"置换算法该淘汰谁" |
缺口二:发现 P=0 时,怎么把控制权交给 OS,处理完还能接着算? ⇒ 中断机制。这里要求的不是普通中断:它必须能在指令执行期间触发,且保存的现场要足以让这条指令从头重新执行。满足这两条的正是内中断里的故障(Fault)类——它的返回语义就是"修复后重新执行当前指令"。详见中断和异常的处理。
缺口三:调入之后,怎么接着完成这次地址变换? ⇒ 地址变换机构。基本分页的变换是一条直路(拆页号 → 查表 → 拼物理地址),请求分页要在中间插一条回路:查到 P=0 就转去缺页中断处理,调入页面、更新页表和快表,再回到起点重新变换一次。所以它不只是"多查一位",而是流程本身多了一条分支和一次重入。
四、请求分段比请求分页多了什么
| 字段 | 作用 | 与请求分页对应吗 |
|---|---|---|
| 存取方式 | 按段的逻辑属性做保护(只执行 / 只读 / 读写) | 请求分页没有——段是逻辑单位才谈得上属性 |
| 访问字段 A | 供置换算法参考 | 对应 |
| 修改位 M | 换出时要不要写回 | 对应 |
| 存在位 P | 该段是否已调入内存 | 对应 |
| 增补位 | 本段运行中是否做过动态增长 | 请求分段特有——段长可变,栈段、堆段会长大 |
| 外存始址 | 该段在外存的起始盘块号 | 对应 |
缺段中断与缺页中断的机制一致(都在指令执行期间产生、一条指令可能触发多次),差别只在"跨块"这一条:段是信息的逻辑单位,一条指令不会被切到两个段里;页则可能跨页。这条差别的直接后果,就是两者对"最少需要几个存储块"的要求不同——见页框分配与回收。
五、虚拟内存 vs 基本分页
| 比较 | 基本分页 | 请求分页(虚拟内存) |
|---|---|---|
| 装入方式 | 一次性全部装入 | 按需调入(缺页时调入) |
| 是否需要全部在内存 | 是 | 否 |
| 页表项 | 页号 → 页框号 | 增加状态位 P、访问字段 A、修改位 M、外存地址 |
| 缺页处理 | 无此概念 | 触发缺页中断(内中断·故障类),调入后重新执行该指令 |
| 页面置换 | 无 | 内存满时需要置换 |
| 逻辑地址空间上限 | ≤ 物理内存 | ≤ min(内存+外存, 地址位数决定的寻址空间) |
六、一次访存里,硬件、OS、编译器各干了什么
虚存的运转是三方分工的,题目会挑其中一环问"这一步归谁"。分工原则只有一句: 简单且每次访存都要做的交硬件,复杂但罕见的交 OS,跟运行期状态无关的才轮到编译器。
| 环节 | 归谁 | 为什么是它 |
|---|---|---|
| 生成逻辑(虚拟)地址 | 编译器 / 链接器 | 这是静态的事,与运行时装到哪无关 |
| 分配物理页框、填写页表项 | 操作系统 | 页框归谁要看运行时的全局状态,编译器无从知道 |
| 地址转换(虚地址 → 物理地址) | 硬件 MMU + TLB | 每条访存指令都要做一次,交给软件的话每次访存都得跑一段程序 |
| 检测缺页、越界、越权 | 硬件 | 检查和地址变换在同一条流水线上,顺手就做了 |
| 处理缺页与其他异常 | 操作系统 | 要找空闲页框、选淘汰页、读磁盘、改页表——全是复杂决策,且发生频率低 |
四处最容易答反的:地址转换不是 OS 做的(是 MMU);页表项不是编译器填的(是 OS); 缺页中断不是硬件"处理"的(硬件只负责触发,处理是 OS); 而异常一律由 OS 处理——缺页、越权、非法指令、除零都遵循 "硬件检测 → 触发异常 → OS 处理"这同一个模式。
考点速记
- 虚拟内存要打破的前提是"进程必须全部装入内存",它卡死了单进程规模与并发度;而进程里有大量很少甚至从不执行的代码全程占着内存。
- 理论基础是局部性原理:时间局部性(刚访问过的很快还会访问)+ 空间局部性(刚访问位置的附近很快也会访问)。⚠️ 落点是"平摊下来可接受",不是"没有代价"——可行性建立在"真实程序有局部性"这个经验事实上,不是逻辑必然。
- 三个特征是一条链:多次性(进得来)+ 对换性(出得去)⇒ 才产生虚拟性(目的,也是最本质的特征)。前两者是机制,虚拟性是效果。
- 三项硬件支持各补一个缺口:页表机制负责"知道"(状态位 P、外存地址、修改位 M、访问字段 A)、中断机制负责"救援"、地址变换机构负责"重来"。
- 缺页中断必须是故障(Fault)类内中断。普通中断在指令执行完毕后才检测、返回执行下一条;缺页是指令执行到一半(正取操作数)才发现的,那条指令根本没做完,处理完必须重新执行它。
- 虚拟内存必须建立在离散分配之上。连续分配要求整个进程占一整块连续物理空间,"部分装入"在它那里根本不成立。
- 容量上限
内存 外存总容量,地址位数决定的寻址空间 。 所以"虚拟内存 = 外存大小"是错的——32 位地址决定单进程寻址上限 4 GB,哪怕装 1 TB 硬盘也不会超过 4 GB。 - 软硬件分工:地址转换归硬件 MMU,页表项由OS填写(不是编译器),缺页由硬件检测、OS 处理,一切异常都由 OS 处理。
- 三种实现里请求分页最常用(换入换出单位定长);请求分段特有存取方式与增补位;只有分页会跨块——段是信息的逻辑单位,不可能出现一条指令被分割在两个段中。
这一节在真题里被考过的形式:
三道题,每一道都是"判断四条叙述"的形式,考的全是概念边界,没有计算。 可以说这一节的全部考法就是"把虚存的几条性质说反,看你认不认得出来"。
- 判断虚拟存储器的四条叙述(2012-25)。答虚拟存储只能基于非连续分配技术。⚠️ 另三条把两个上限都说错了:"只受外存容量限制"和"只受内存容量限制"都是错的,正确的是两者之和与地址位数取小(速记第七条);"只能基于连续分配"更是反的。
- 判断进程虚拟地址空间的四条叙述,选错误的(2023-28)。错项是"虚拟地址的大小由主存和硬盘的大小决定"——虚拟地址位数由体系结构定死,跟你装多少内存硬盘无关。另三条都对:每个进程有独立的虚拟地址空间、
malloc()返回的是虚拟地址、数据段和代码段可以有不同访问权限。 - 判断虚存系统下指令执行过程的四条叙述(2026-24)。答异常由操作系统处理。⚠️ 另三条正好把分工全说反了:地址转换由 MMU 硬件做(不是 OS)、页表项由 OS 填(不是编译器)、缺页由硬件触发但由 OS 处理(不是硬件直接处理)。这道题就是速记第八条的完整考法。
复习优先级:必须拿满,且只需记边界不需要算。 速记第七条(两个上限)和第八条(软硬件分工) 是三道题里两道的直接答案。第五条(缺页为什么必须是 Fault)会在 请求页式管理再用一次。局部性原理那个反例读一遍建立直觉即可, 不会单独设问。
易错:认为虚拟内存的容量等于外存大小、或等于内存加外存。上限是两者之和与地址位数决定的寻址空间取小。
易错:认为虚拟地址的大小由装了多少内存和硬盘决定。它由体系结构的地址位数定死。
易错:认为地址转换由操作系统完成。是硬件 MMU——每条访存指令都要做一次,软件做不起。
易错:认为页表项的内容由编译器确定。页框归谁是运行时的全局状态,只能由 OS 填写。
易错:认为缺页中断由硬件直接处理。硬件只负责触发,找页框、选淘汰页、读盘、改页表都是 OS 做的。
易错:认为虚拟内存可以建立在连续分配之上。必须是离散分配,否则"部分装入"根本不成立。
易错:把缺页中断当成普通的外中断。它是故障(Fault)——在指令执行中途触发,处理完要重新执行该指令。
易错:认为虚拟内存没有代价、是纯粹的改进。它永远比全部装入慢,换来的是能跑更大的程序、更多的进程。
教材出处
- 请求分段所需的三项硬件支持(请求段表机制、缺段中断机构、地址变换机构)、段表项的六个新增字段(存取方式、访问字段 A、修改位 M、存在位 P、增补位、外存始址)、以及"分段是信息的逻辑单位,不可能出现一条指令被分割在两个分段中":汤小丹《计算机操作系统》5.5.1 节「请求分段中的硬件支持」,p173
相关知识
段页式管理|请求页式管理|运行时内存映像与地址空间|中断和异常的处理