Skip to content

第五章 内存管理:分页、分段与虚拟内存


1. 为什么需要地址转换

第二章说过,每个进程都以为自己独占一条从 0 开始的内存。这当然是假象——物理内存只有一条,凭什么每个进程都觉得自己从地址 0 开始?

答案:程序里写的地址是逻辑地址(也叫虚拟地址,程序视角的门牌号),真正访问内存时由硬件把它翻译成物理地址(内存条上的真实位置)。这次翻译不是多此一举,它一口气解决三个问题:

  1. 重定位:程序编译时不知道自己将来被装进内存哪个位置。有了翻译层,程序统一按"从 0 开始"编址,装到哪儿都能跑。
  2. 保护:每个进程只能通过自己的翻译表访问自己的内存,翻译表里没有的地址一律拦截——进程再嚣张也摸不到别人的数据。
  3. 共享:想让两个进程共用一段代码(比如同一个库),只需让两张翻译表指向同一块物理内存。

一句话点透:逻辑地址是操作系统发给每个进程的"专属幻觉",翻译由硬件中的内存管理单元 MMU 在每次访存时自动完成

2. 连续分配与碎片

最朴素的做法:给每个进程分一段连续的物理内存。分法有固定分区(预先切成固定大小的格子)和动态分区(按需切割)。动态分区释放回收后,内存里会留下大小不一的空闲块,于是有了"挑哪块"的策略:

策略规则优缺点
首次适应从头找,遇到第一块够大的就用简单,通常综合表现不差;低地址端易碎
最佳适应挑能装下的最小空闲块听着节省,实际制造大量装不了东西的小碎屑
最坏适应挑最大的空闲块切剩下的仍较大还能用;但大块很快被消耗殆尽

注意反直觉结论:最佳适应不是最佳——它专门产生极小的、几乎无法再利用的空闲碎屑。

2.1 内碎片与外碎片辨析(高频考点)

碎片类型定义产生场景白话比喻
内碎片分给进程的块内部用不完的部分固定分区、分页4 人开了 6 人包间,空 2 座但别人不能坐
外碎片空闲区太零散,总量够但没有一块连续的够大动态分区、分段停车场空位总面积够停卡车,可惜全是零散小格

记法:分固定大小的都产生内碎片,按需分配连续区域的都产生外碎片。外碎片可用"紧凑"(把进程挪到一起拼出大空闲区)缓解,但搬家开销大。想根治外碎片,就要放弃"连续"这个执念——这就引出了分页。

3. 分页机制:化整为零

思想:把逻辑地址空间切成固定大小的(如 4KB),把物理内存切成同样大小的页框(块)。任何一页可以放进任何一个页框,页与页在物理上不必连续。每个进程一张页表,记录"第几页放在了第几框"。

外碎片就此消失(所有块一样大,任何空框都能用);代价是每个进程最后一页平均浪费半页——退回了少量内碎片。这笔交易非常划算。

3.1 地址转换:手算例题

逻辑地址被机械地拆成两段:页号 = 逻辑地址 ÷ 页大小(取商),页内偏移 = 逻辑地址 mod 页大小(取余)。翻译时页号换成页框号,偏移原样拼接。

例题:页大小 1KB(1024 字节),页表为:0 号页 → 2 号框,1 号页 → 5 号框,2 号页 → 8 号框。求逻辑地址 2500 的物理地址。

  1. 页号 = 2500 ÷ 1024 = 2(商),页内偏移 = 2500 − 2 × 1024 = 452。
  2. 查页表:2 号页在 8 号框。
  3. 物理地址 = 8 × 1024 + 452 = 8644

若给的是二进制/十六进制,套路相同:页大小 2 的 n 次方,则逻辑地址低 n 位是偏移、高位是页号,翻译时只换高位、低位照抄。

3.2 页表放哪儿:TLB 为什么能加速

页表本身放在内存里。这带来一个尴尬:每访问一个数据,都要先访存查页表、再访存取数据——访存次数翻倍

解药是快表 TLB(Translation Lookaside Buffer,白话:MMU 里的一小块超高速缓存,存最近用过的"页号→框号"对照条目)。访存时先查 TLB:命中则直接得到框号,不用再读内存里的页表;未命中才去查页表,并把结果填入 TLB 备用。

TLB 为什么有效?因为程序访存有局部性——短时间内反复访问同一批页,几十条 TLB 条目就能覆盖绝大多数访问。设 TLB 命中率 90%、查 TLB 忽略不计、访存一次 100ns,则平均访存时间 = 0.9 × 100 + 0.1 × 200 = 110ns,远优于无 TLB 的 200ns。

多级页表一句话:进程地址空间很大时页表本身太大,就把页表也切页、再建一张"页表的页表",按需加载,用时间换空间。

4. 分段与段页式

4.1 分段:按逻辑意义划分

分页是操作系统按固定大小机械切割,程序员无感。分段则按程序的逻辑单元划分:代码段、数据段、栈段各成一段,每段长度不同,逻辑地址 =(段号,段内偏移)二维表示。段表记录每段的起始地址(基址)和长度(界限),翻译时检查偏移不得越过段长。

4.2 分页 vs 分段对照表(高频考点)

对比项分页分段
划分依据物理需要,固定大小机械切逻辑意义,按程序结构切
大小页大小固定,由系统定段长不定,由程序决定
对程序员透明,不可见可见,编程可按段引用
地址形式一维:一个数字拆页号加偏移二维:必须给出段号和段内偏移
碎片只有少量内碎片有外碎片,无内碎片
共享与保护以页为单位,不贴合逻辑边界以段为单位,天然贴合,更方便

段页式:先按逻辑分段,段内再分页装入——共享保护看段、内存利用看页,两全其美;代价是逻辑地址要经过段表、页表两次查表,访存次数更多。

5. 虚拟内存:借磁盘扩内存

到目前为止仍假设进程整个装入内存。但观察发现:程序运行遵循局部性原理——时间局部性(刚用过的很快还会再用,如循环变量)与空间局部性(用了某处,附近的很快也会用到,如顺序扫描数组)。既然任一时刻真正活跃的只是一小部分页,何必全部装入?

虚拟内存由此而来:只装入当前需要的页,其余留在磁盘上;访问到不在内存的页时再临时调入。这就是请求分页。效果:进程看到的地址空间可以远大于物理内存,更多进程得以同时驻留内存。

虚拟内存的三个特征:多次性(作业分多次调入)、对换性(页可换入换出)、虚拟性(逻辑容量大于物理容量)。它的物质基础是"页表 + 缺页中断机制 + 磁盘对换区"。

6. 缺页中断处理流程

访问的页不在内存时,MMU 触发缺页中断(属于第一章讲的内中断/异常),由操作系统补救:

两个细节常考:其一,牺牲页只有被修改过(脏页)才需写回磁盘,没改过的磁盘上本来就有副本,直接覆盖即可——页表里的"修改位"就是干这个的;其二,处理完后重新执行引起缺页的那条指令,而不是跳到下一条。

7. 页面置换算法:手算例题

内存满了要"挑谁滚蛋",挑得不好,刚踢走的马上又要用。约定例题统一使用引用串 7 0 1 2 0 3 0 4 2 3 0 3,内存 3 个页框,初始为空。注意:首次装入某页也算一次缺页

7.1 OPT 最佳置换:理论天花板

规则:淘汰将来最长时间不会被访问的页。需要预知未来,无法实现,只作为衡量其他算法的标尺。

访问701203042303
内存77 07 0 12 0 12 0 12 0 32 0 32 4 32 4 32 4 32 0 32 0 3
缺页

推演关键步:访问 2 时淘汰 7(7 此后再不出现);访问 3 时淘汰 1(1 再不出现);访问 4 时比较未来——2 在第 9 次、3 在第 10 次、0 在第 11 次才被用到,0 最远,淘汰 0;第 11 次访问 0 时缺页,淘汰再也不用的 4。共缺页 7 次。哪怕 0 将来还要用,OPT 也照淘不误——它只看"谁最晚才被用",不看"还用不用"。

7.2 FIFO 先进先出:谁来得早谁走

规则:淘汰在内存中待得最久的页。实现只需一个队列,但"住得久"不等于"没用了"。

访问701203042303
内存77 07 0 12 0 12 0 12 3 12 3 04 3 04 2 04 2 30 2 30 2 3
缺页

共缺页 10 次。注意第 6、7 次:淘汰 0 之后紧接着又要访问 0,只好再把 3 踢换回 0——FIFO 的典型愚蠢时刻。

Belady 异常:FIFO 独有的怪病——给它更多页框,缺页反而可能更多(经典反例引用串 1 2 3 4 1 2 5 1 2 3 4 5:3 框缺 9 次,4 框缺 10 次)。OPT 和 LRU 永远不会发生 Belady 异常。

7.3 LRU 最近最久未使用:用过去预测未来

规则:淘汰最长时间没被访问过的页。依据正是局部性原理:最近没用的,近期大概率也不用——这是"用历史模仿 OPT"。

访问701203042303
内存77 07 0 12 0 12 0 12 0 32 0 34 0 34 0 23 0 23 0 23 0 2
缺页

第 8 次访问 4 时,最久未用者是 2?查历史:最近使用序为 0(第 7 次)、3(第 6 次)、2(第 4 次),故淘汰 2;第 9 次访问 2 又缺,淘汰最久未用的 3;第 10 次访问 3 再缺,淘汰 4。共缺页 9 次。手算技巧:从当前位置向左回看,最晚出现的那个页就是牺牲品

LRU 效果好但实现贵——严格实现要给每页记时间戳或维护访问栈,硬件开销大,于是有了下面的近似版。

7.4 Clock 时钟算法:LRU 的廉价近似

规则:所有页排成环,每页一个访问位(被访问过置 1)。指针扫描:遇到访问位为 1 的,清零并放它一马、指针前移;遇到 0 的,就淘汰它。白话:给每页一次缓刑机会,第二圈还没被用过就出局。改进型再加修改位,优先淘汰"既没被访问又没被修改"的页,省一次写盘。

算法本例缺页数定位
OPT7理论最优,不可实现,用作标尺
FIFO10最容易实现,性能差,有 Belady 异常
LRU9性能接近 OPT,实现开销大
Clock介于 FIFO 与 LRU 之间工程折中,实际系统主流选择

8. 抖动与工作集

抖动(颠簸):分给进程的页框太少,装不下它当前活跃的页集合,于是刚换出的页马上又被访问——系统把绝大部分时间花在换页的磁盘 I/O 上,CPU 利用率骤降。危险的连锁反应:CPU 利用率低 → 系统以为负载不足 → 调入更多进程 → 每个进程页框更少 → 抖动更凶。

工作集:进程在最近一个时间窗口内实际访问的页面集合,即"当前活跃页"的度量。防抖动的原则:分给进程的页框数不低于其工作集大小;内存装不下这么多进程的工作集,就减少多道程序数量(挂起部分进程),而不是继续硬塞。

9. 本章要点回顾

主题必须记住的结论
地址转换解决重定位、保护、共享三件事,由 MMU 硬件完成
碎片固定切割产生内碎片,连续按需分配产生外碎片;最佳适应不是最佳
分页手算页号取商、偏移取余;偏移位数由页大小决定,翻译只换高位
TLB缓存页表条目,靠局部性获得高命中率,减少访存次数
分页 vs 分段分页固定、透明、一维、内碎片;分段逻辑、可见、二维、外碎片
虚拟内存局部性原理是理论根基;多次性、对换性、虚拟性
缺页处理脏页才写回;处理完重新执行原指令
置换算法OPT 标尺、FIFO 有 Belady 异常、LRU 近似最优、Clock 工程折中
抖动页框少于工作集所致;解法是减少并发进程数而非加大调入

10. 做题提醒

  1. 数缺页次数时,初始装入也算缺页,别只数置换发生的次数。
  2. 页大小为 2 的 n 次方时,逻辑地址低 n 位直接抄到物理地址——考题若选项里偏移变了,直接排除。
  3. Belady 异常只属于 FIFO;说 LRU 或 OPT 有 Belady 异常的选项都是错的。
  4. 基本分页系统一次访存的数据访问需要 2 次访问内存(查页表 + 取数据),段页式要 3 次,TLB 命中则减为 1 次。
  5. 分页对程序员透明、地址一维;分段对程序员可见、地址二维——"分页地址是二维的"是经典错误表述。
  6. 缺页率高不一定是算法差,先看页框数是否小于工作集;抖动的对策是降低多道程序度,选"增加进程数"的选项必错。