408 · 操作系统 · 第 3 章(下)

虚拟内存

上半章的所有方案都要求进程整个装进内存才能运行。这一半把这个要求拿掉:只装当前要用的页,访问到不在内存的页再调进来,内存满了就挑一页换出去。难点集中在两处:缺页时到底发生了什么,以及挑哪一页换出去。

为什么只装一部分就能跑局部性原理:一段时间内进程只访问少数几页。缺哪页调哪页,暂时不用的页可以换出
缺页 = 页表项状态位 P = 0缺页中断是内中断(故障),在指令执行中途产生,处理完重新执行这条指令
置换算法比的是缺页次数OPT 看未来(最优,做不到);LRU 看过去(最接近 OPT);FIFO 只看进来的早晚,会出现 Belady 异常
一 · 演进导图

每一步都在解决上一步留下的问题

上接 ← 内存管理:基本分页已经做到“进程拆成页、分散放”,但仍要求所有页一次全部装入内存。虚拟内存就是在基本分页上加一步:页可以不在内存。

虚拟内存用“内存 + 外存”在逻辑上扩充内存容量,让大作业能跑、让更多进程同时驻留
为什么能只装一部分从传统存储管理的两个毛病讲起

传统存储管理

一次性驻留性
做法
作业必须一次全部装入内存才能开始运行;装入后一直驻留到运行结束
新问题
比内存大的作业根本跑不了;同时能装的作业少;暂时用不到的代码和数据也白白占着内存

局部性原理

理论依据
时间局部性
刚执行过的指令、刚访问过的数据,不久后很可能再被访问(循环)
空间局部性
访问了某个地址,附近的地址很快也会被访问(顺序执行、数组)
推论
一段时间内只需要进程的一小部分在内存

虚拟内存

多次性对换性虚拟性
做法
只装当前要用的部分就开始运行;缺什么调什么;内存不够就把暂时不用的换到外存
容量
最大容量由地址位数决定(2n);实际容量 = min(内存 + 外存, 2n)
代价
换入换出要访问磁盘,比访存慢几个数量级;需要额外的硬件和 OS 支持

实现方式

三种
请求分页(最常用,本页重点)、请求分段、请求段页式
前提
只能建立在离散分配上;连续分配做不了虚拟内存
硬件
一定容量的内存和外存、页表机制、缺页中断机构、地址变换机构
请求分页怎么做基本分页 + 请求调页 + 页面置换

请求分页页表

做法
页表项在“页号 → 块号”之外,加上状态位 P、访问字段 A、修改位 M、外存地址
解决
知道每页在不在内存、不在时去外存哪里找、换出时要不要写回

缺页中断

内中断 · 故障
做法
访问的页 P = 0 → 发缺页中断 → OS 把该页从外存调入
新问题
内存里没有空闲块了,换出谁?

页面置换算法

目标
缺页次数尽量少(换出去的页最好很久都不再用)
代价
越接近 OPT 的算法,硬件和维护开销越大

页面分配与置换策略

问题
每个进程给几个物理块(驻留集)?换页时能不能拿别的进程的块?

抖动

块给少了
现象
刚换出的页马上又要用,刚换入的页马上又被换出;CPU 大部分时间在等换页
原因
分配给进程的物理块数 < 它频繁访问的页数

工作集

驻留集 ≥ 工作集
做法
统计最近窗口 Δ 内访问过的页面集合,按它的大小决定给多少块
作用
驻留集不小于工作集,就不会抖动

下接 → 文件管理:第九节的内存映射文件把文件映射进虚拟地址空间,用缺页机制读文件,是虚拟内存和第 4 章的交界。

二 · 虚拟内存的特征与容量

三个特征,正好对着传统方式的两个毛病

特征含义针对传统方式的哪个毛病
多次性作业不用一次全部装入,允许分多次调入内存(最重要的特征)一次性
对换性作业运行时不必一直常驻内存,允许换入、换出驻留性
虚拟性逻辑上扩充了内存容量,用户看到的内存远大于实际内存前两个特征带来的结果

容量怎么算

最大容量看地址位数;实际容量取两者较小值:min(内存 + 外存, 2地址位数)

例:32 位地址(232 B = 4 GB),内存 512 MB,外存可用 2 GB。

  • 最大容量 = 4 GB(地址只能编这么多)
  • 实际容量 = min(0.5 + 2, 4) = 2.5 GB

如果外存换成 8 GB,实际容量就是 min(8.5, 4) = 4 GB:外存再大,地址也编不出来。

为什么只能基于离散分配

连续分配要求进程在内存里占一整段。只装一部分、按需调入,意味着每次调进来的页落在哪个空闲块都不确定,必须靠页表逐页记录位置。

所以虚拟内存的三种实现方式都是离散的:请求分页、请求分段、请求段页式。

“虚拟性”靠多次性和对换性实现,而这两者又都建立在离散分配上。

三 · 请求分页的页表项

每个新字段都有一个固定的使用者

记字段时就问一句:谁在什么时候读它。P 给地址变换机构,A 给置换算法,M 给置换时的写回判断,外存地址给缺页处理程序。

字段记录什么给谁用什么时候改
页号页表下标,实际不占存储(和上半章一样)定位表项:页表起址 + 页号 × 表项长度—
物理块号该页在内存中的块号,P = 1 时才有效地址变换:块号 ‖ 页内偏移调入时填写
状态位 P(存在位)该页是否已调入内存地址变换机构:P = 0 就发缺页中断调入置 1,换出置 0
访问字段 A最近是否被访问过 / 被访问次数 / 多久没被访问置换算法选淘汰页(LRU、CLOCK)访问时由硬件置位或更新;CLOCK 扫描时清 0
修改位 M(脏位)调入内存后是否被写过置换时决定要不要写回外存写操作时由硬件置 1
外存地址该页在外存上的位置,通常是外存块号缺页处理程序调入该页时按它去读换出到对换区时可能改变
M = 0 的页被换出外存上还有一模一样的副本,直接覆盖这个块,不用写回
M = 1 的页被换出内存里的版本比外存新,必须先写回,多一次磁盘 I/O
为什么改进型 CLOCK 要看 M同样“最近没用过”,优先换没改过的页,省掉写回的开销
四 · 缺页中断与地址变换

从给出逻辑地址到拿到数据,完整走一遍

绿色是快表命中的捷径,红色是缺页路径。缺页处理结束后,不是接着往下走,而是回到开头把这条指令重新执行一遍。

是 否 是 否 是 否 是 否 否 是 中断返回:重新执行引起缺页的那条指令(再查快表就命中了) 重新给出逻辑地址 CPU 给出逻辑地址 拆成页号 P ‖ 页内偏移 W 页号 ≥ 页表长度? 越界中断 不再往下查 查快表,命中? 直接取出块号 省掉一次查页表的访存 查内存中的页表 第 1 次访存 状态位 P = 1? 把该页表项写入快表 快表满了就按替换算法换掉一项 块号 ‖ W → 物理地址 访存取数据(写操作则置 M = 1) 发出缺页中断 内中断(故障)· 进程阻塞 有空闲块? 置换算法选出淘汰页 OPT / FIFO / LRU / CLOCK 淘汰页 M = 1? 把淘汰页写回外存 没改过就直接覆盖,不写回 按外存地址调入该页 启动磁盘 I/O,CPU 可调度别的进程 修改页表和快表 P = 1,填块号;进程转就绪 访存次数(一级页表) 快表命中:1 次(只取数据) 快表未命中、页在内存:2 次(查页表 + 取数据) 缺页:查页表 1 次 + 缺页处理 + 重新执行时 再取数据 1 次(此时快表已命中)

缺页中断属于哪一类

内中断(异常)里的故障(fault)。它由当前指令访问了不在内存的页引起,和外设无关;而且是可以修复的错误,修好了这条指令就能正常执行。

  • 故障:返回后重新执行引起异常的那条指令(缺页就是这种)
  • 陷入(trap,系统调用):返回后执行下一条指令
  • 终止:不可恢复,不再返回

和一般中断的两点不同

  • 在指令执行期间产生和处理。一般中断是一条指令执行完后才检查;缺页发生在取指令或取操作数的半路上,不处理这条指令就进行不下去。
  • 一条指令可能多次缺页。例如一条跨两页的指令,把一个跨两页的数据块 A 复制到同样跨两页的 B,最多可能缺页 6 次。

缺页处理做了什么

  1. 保护 CPU 现场,进程由运行态变为阻塞态(要等磁盘 I/O)。
  2. 根据页表项里的外存地址找到该页。
  3. 有空闲块就直接分配;没有就用置换算法选一页淘汰,淘汰页 M = 1 要先写回外存,并把它的页表项 P 置 0。
  4. 启动 I/O 把页调入。调入期间 CPU 去运行别的进程。
  5. 修改页表(P = 1,填块号)并更新快表,进程转为就绪。
  6. 再次被调度时,重新执行那条引起缺页的指令。
五 · 页面置换算法

区别只在“凭什么判断这页以后不用了”

OPT 看未来,LRU 看过去的使用时间,FIFO 只看进入内存的先后,CLOCK 用一个访问位粗略近似 LRU。先想清楚依据,特点和代价都能推出来。

OPT 最佳置换

淘汰以后永远不用、或者最长时间内不再被访问的页

特点缺页率最低,是所有算法的上限

代价无法实现:OS 不可能预知进程以后的访问序列。只用来当标准,评价其他算法

FIFO 先进先出

淘汰最早进入内存的页(队列头)

特点实现最简单,一个队列就够

代价进来得早不代表用得少,常用的页(如主循环所在页)也会被换出,性能差;会出现 Belady 异常:分配的物理块增加,缺页次数反而增加

LRU 最近最久未使用

淘汰过去最长时间没被访问的页

特点用“过去”估计“未来”(时间局部性),性能最接近 OPT;属于栈式算法,不会出现 Belady 异常

代价每次访问都要更新使用时间,需要寄存器和栈等硬件支持,开销大

CLOCK 时钟(NRU 最近未用)

淘汰访问位 A = 0 的页

做法内存中的页连成循环队列,指针依次扫描:A = 1 就清 0、跳过(给第二次机会),A = 0 就换出。新页装入时 A = 1,指针指向下一块。

轮数第一轮可能把所有 A 都清成 0,第二轮一定找到,最多两轮

改进型 CLOCK:同时看访问位 A 和修改位 M

同样是最近没用过,换出没改过的页不用写回磁盘,更省。按 (A, M) 把页分成四类:

类(A, M)含义淘汰优先级
第 1 类(0, 0)最近没访问,也没修改最先淘汰,换出不用写回
第 2 类(0, 1)最近没访问,但修改过其次,换出要写回
第 3 类(1, 0)最近访问过,没修改可能很快再用
第 4 类(1, 1)最近访问过,也修改过最后才淘汰
  1. 第 1 轮:从指针处扫描,找 (0, 0),不改任何标志位。找到就换出。
  2. 第 2 轮:找 (0, 1),扫描过的页 A 都清 0。找到就换出。
  3. 第 3 轮:此时所有页 A 都是 0,重新找 (0, 0)(原来的第 3 类变成了第 1 类)。
  4. 第 4 轮:再找 (0, 1),一定能找到。所以最多四轮。
六 · 动手看 · 页面置换模拟器

同一串访问,四种算法各缺页几次

默认是教材上最经典的一串,分 3 个物理块。切换算法,单步往前看每次淘汰的理由;也可以改访问串和块数。

约定:物理块初始为空;前几次访问把页装进空闲块,也计缺页,但不算置换。所以 置换次数 = 缺页次数 − 被填满前的装入次数。

七 · 页面分配与置换策略

给几块、能不能抢别人的块、什么时候调、从哪儿调

驻留集:请求分页中,分配给一个进程的物理块的集合。驻留集太小,缺页频繁;太大,能同时装入的进程变少,并发度下降。

策略驻留集大小缺页且没空闲块时问题
固定分配 局部置换运行前分好,运行期间不变只能换出自己的页很难事先确定该给多少:给少了频繁缺页,给多了浪费
可变分配 全局置换可变:缺页时从 OS 的空闲块队列再分一块空闲块用完后,可以换出系统中任一进程的页(未锁定的)被换出页的进程块数变少,它的缺页率上升;进程之间互相影响
可变分配 局部置换可变:根据缺页率动态调整只换出自己的页;缺页频繁就多给几块,缺页率很低就收回一些需要持续统计缺页率,实现较复杂,但效果最好
没有“固定分配全局置换”全局置换要从别的进程拿块,自己的块数就变了,和“固定”矛盾
初始分几块平均分配、按进程大小比例分配、按优先级分配

什么时候调入

  • 预调页:根据空间局部性,一次调入若干相邻的页。预测不一定准(成功率约 50%),主要用于进程首次调入,由程序员指出先调哪些页。
  • 请求调页:运行中发现缺页才调入,每次只调一页。调入的一定会被用到,但每次都要一次 I/O,开销大。

从哪儿调入

外存分两块:对换区连续分配,读写快;文件区离散分配,读写慢。

  1. 对换区空间足够:运行前把进程相关文件从文件区复制到对换区,以后全部从对换区调入。
  2. 对换区空间不够:不会被修改的页直接从文件区调入,换出时也不用写回;可能被修改的页,换出时写到对换区,以后从对换区调回。
  3. UNIX 方式:没运行过的页从文件区调入;运行过又被换出的页放在对换区,下次从对换区调入。共享页如果已被别的进程调入,就不用再调。
八 · 抖动与工作集

块给少了会怎样,给多少才够

抖动(颠簸)

现象刚换出的页马上又要访问,只好再换入;刚换入的页又被换出。进程大部分时间花在换页上,CPU 利用率骤降。

根本原因分配给进程的物理块不够:它频繁访问的页面数高于可用的物理块数。置换算法选得不好也会加剧。

怎么办给进程更多块(让驻留集 ≥ 工作集);块不够时降低多道程序度,挂起(换出)部分进程,把腾出的块分给其余进程。

工作集

定义在某段时间间隔(窗口 Δ)里,进程实际访问过的页面集合,记作 W(t, Δ)。

大小工作集大小 ≤ Δ,局部性越好,工作集越小(重复访问同几页)。

分配的物理块数(驻留集)不能小于工作集大小,否则就会频繁缺页,走向抖动。也可以用它选淘汰页:优先换出不在工作集里的页。

算一个:窗口 Δ = 4

时刻 t12345678910工作集
访问页1232144454
W(5, 4)2321{1, 2, 3},大小 3
W(10, 4)4454{4, 5},大小 2

往回数 Δ 次访问(含当前这次),去重就是工作集。t = 10 时进程集中在页 4 附近,工作集只有 2 页,这时给它 2 块就够;t = 5 时至少要 3 块。

九 · 内存映射文件 & 与 Cache 的对照

同一套“缓存 + 换入换出”的思路,用在了三个地方

内存映射文件(mmap)

OS 把一个磁盘文件映射到进程虚拟地址空间的一段,进程之后像访问内存一样读写文件,不用再调 read / write。

  • 怎么读进来:映射时并不读文件。访问到哪一页,发生缺页,OS 才把文件对应的那一页调入内存。
  • 怎么写回去:修改过的页在关闭文件、或者被换出时写回磁盘文件。
  • 好处:简化文件访问编程;多个进程可以把同一个文件映射到各自的虚拟地址空间,映射到同一组物理页,实现共享。
对比项Cache ↔ 主存(计组)主存 ↔ 辅存(虚拟内存)快表 TLB
目的解决主存速度不够解决主存容量不够加快地址变换
缓存的内容主存块(几十字节)页(KB 级)页表项
由谁管理全部由硬件完成硬件(地址变换、缺页中断)+ OS(调页、置换)硬件
透明性对所有程序员透明对应用程序员透明,对系统程序员不透明对程序员透明
映射方式直接 / 全相联 / 组相联全相联(任一页可放任一块,由页表记录)常用全相联或组相联
不命中的代价访问主存,几十到上百 ns访问磁盘,ms 级,差 5~6 个数量级多访存一次查页表
不命中时 CPU等待,硬件直接从主存调块缺页中断,进程阻塞,CPU 转去运行别的进程硬件或 OS 查页表后装入
写策略全写法或写回法只能写回(修改位 M,换出时才写)—
替换算法硬件实现:随机、FIFO、LRU软件实现:OPT(理论)、FIFO、LRU、CLOCK硬件实现
十 · 有效访问时间 EAT

三种情况分别算时间,再按概率加权

一次访存只可能落进三种情况之一:快表命中、快表未命中但页在内存、缺页。每种情况的时间照着上面的流程图数一遍就出来了。

公式(一级页表,先查快表再查页表)

记快表访问时间 tT,内存访问时间 tm,快表命中率 α,缺页率 p(占全部访问的比例,缺页一定是快表未命中,所以 p ≤ 1 − α),缺页处理时间 Tf。

① 快表命中
t1 = tT + tm
② 快表未命中,页在内存
t2 = tT + tm + tm
③ 缺页:查快表 + 查页表 + 处理 + 重新执行(快表命中)
t3 = tT + tm + Tf + tT + tm
EAT = α × t1 + (1 − α − p) × t2 + p × t3

算例

tT = 10 ns,tm = 100 ns,α = 0.9,p = 0.00001,Tf = 10 ms = 107 ns。

  1. 0.9 × 110 = 99 ns
  2. (1 − 0.9 − 0.00001) × 210 = 0.09999 × 210 ≈ 20.998 ns
  3. 0.00001 × (10 + 100 + 107 + 10 + 100) ≈ 100.002 ns
EAT ≈ 99 + 20.998 + 100.002 = 220 ns

十万次访问里才缺一次页,却贡献了将近一半的时间。缺页处理(磁盘 I/O)太贵,所以缺页率必须压得非常低。

题目的约定会变:有的说快表和页表同时查(未命中时不再加 tT);有的说缺页处理后直接访存而不是重新查快表;有的不考虑快表。先按题意把每种情况的步骤数出来,再套加权。常考题型是给几个逻辑地址,分别问每个地址的访问时间,那就是分别落在 ①②③ 哪一种。

计算器

十一 · 易错速记 & 算法速查

选择题直接对表

算法淘汰依据能否实现Belady 异常开销 / 硬件一句话
OPT未来最久不用不能不会—缺页最少,当标准用
FIFO进入内存最早能会最小,一个队列最简单,性能差
LRU过去最久没用能不会大,需要寄存器和栈最接近 OPT
CLOCK / NRU访问位 A = 0能考试按“只有 FIFO”作答小,一位访问位 + 指针最多扫描 2 轮
改进型 CLOCK(A, M) 优先 (0, 0)能同上小,访问位 + 修改位最多扫描 4 轮,少写回
缺页中断是内中断(故障)由指令本身引起,在指令执行期间产生;返回后重新执行本条指令,不是下一条。一条指令可能多次缺页。
Belady 只出现在 FIFO块数增加,缺页次数反而增加。OPT、LRU 是栈式算法,块多时内存里的页集合一定包含块少时的,所以不会。
LRU 看过去,OPT 看未来两者规则“对称”:LRU 往前找最久没用的,OPT 往后找最久不用的。做题时别看反方向。
驻留集太小会抖动抖动的原因是物理块不够,不是置换算法本身;解决办法是加块或降低多道程序度。驻留集 ≥ 工作集。
虚拟内存只能基于非连续分配请求分页、请求分段、请求段页式;连续分配方式实现不了虚拟内存。
实际容量 = min(内存 + 外存, 2n)最大容量只由地址位数决定,和内存外存多大无关。
修改位 M 决定写回没改过的页换出直接覆盖;改过的必须写回。虚拟内存的写策略只能是写回。
不存在固定分配全局置换三种合法组合:固定局部、可变全局、可变局部。
首次装入也算缺页画表数缺页时,物理块从空装满的那几次都要算;置换次数则不算它们。

关于 CLOCK 与 Belady:CLOCK 在所有访问位都为 1 时退化为 FIFO,严格说也可能出现 Belady 异常(在模拟器里用 CLOCK 跑那串 1,2,3,4,1,2,5,… 就能看到);但王道和 408 选择题的结论是“只有 FIFO 会出现”,考场上照此作答。