# 16. 虚拟存储器 > **课程**: 操作系统 - 存储器管理 > **核心内容**: 虚拟存储器思想、局部性原理、请求分页、页面置换算法、内存分配策略、抖动与工作集 --- ## 前置知识 - [[14_分页存储管理]] — 分页思想、页表、地址变换、缺页中断 - [[15_段式存储管理]] — 分段思想、段页式 - [[11_处理机调度]] — 进程调度基本概念 --- ## 一、虚拟存储器的基本思想 ### 问题的提出 在传统的存储管理中,作业必须**全部装入内存**才能运行。这带来两个问题: 1. 作业大小超过物理内存时,无法运行 2. 内存中同时驻留多个大程序时,内存不够用 ### 虚拟存储器思想 **核心**: 程序员在比实际物理内存**大得多**的虚拟地址空间中编写程序,运行时只需将**部分页面**调入内存,其余留在外存。当访问到不在内存的页面时,再从外存调入。 ```mermaid flowchart LR subgraph VA["虚拟地址空间 (很大)"] direction TB V0["页面0 ✓ 在内存"] V1["页面1 ✓ 在内存"] V2["页面2 ✗ 在外存"] V3["页面3 ✓ 在内存"] V4["页面4 ✗ 在外存"] V5["..."] end subgraph MEM["物理内存 (较小)"] direction TB M0["页框0"] M1["页框1"] M2["页框2"] M3["页框3"] end subgraph DISK["外存 (磁盘)"] direction TB D0["页面2"] D1["页面4"] D2["..."] end V0 --> M0 V1 --> M1 V3 --> M3 V2 -.->|"缺页时调入"| DISK V4 -.->|"缺页时调入"| DISK style VA fill:#e3f2fd,stroke:#1976d2 style MEM fill:#fff8e1,stroke:#f9a825 style DISK fill:#fce4ec,stroke:#c62828 ``` ### 实现基础 虚拟存储器的实现依赖两个关键技术: 1. **[[14_分页存储管理|分页]]或[[15_段式存储管理|分段]]**: 将程序划分为小块,可以部分装入 2. **[[14_分页存储管理#五、地址变换过程|缺页中断]]**: 访问不在内存的页面时,由OS负责调入 --- ## 二、局部性原理 虚拟存储器之所以可行,是因为程序运行时具有**局部性**——程序不会均匀地访问所有地址空间。 ### 时间局部性 (Temporal Locality) > **含义**: 如果一条指令/数据被访问,那么在不久的将来它很可能**再次被访问**。 **典型场景**: - 循环中的指令反复执行 - 频繁访问的变量 - 栈顶数据的反复操作 ### 空间局部性 (Spatial Locality) > **含义**: 如果一个存储单元被访问,那么它**附近的单元**也很可能很快被访问。 **典型场景**: - 顺序执行的指令 - 数组的顺序遍历 - 结构体成员的连续访问 ### 局部性的实际影响:行优先 vs 列优先 以二维数组遍历为例,行优先和列优先的性能差异可达 **21.5 倍**: ```c // 二维数组: int a[1024][1024]; // 行优先遍历 (空间局部性好) for (int i = 0; i < 1024; i++) for (int j = 0; j < 1024; j++) sum += a[i][j]; // 访问顺序: a[0][0], a[0][1], a[0][2], ... // 连续内存访问,充分利用缓存行 // 列优先遍历 (空间局部性差) for (int j = 0; j < 1024; j++) for (int i = 0; i < 1024; i++) sum += a[i][j]; // 访问顺序: a[0][0], a[1][0], a[2][0], ... // 每次跳过一整行,频繁缺页/缓存失效 ``` ```mermaid flowchart LR subgraph RowMajor["行优先遍历"] direction LR R1["a[0][0]"] --> R2["a[0][1]"] --> R3["a[0][2]"] --> R4["..."] style R1 fill:#c8e6c9 style R2 fill:#c8e6c9 style R3 fill:#c8e6c9 end subgraph ColMajor["列优先遍历"] direction LR C1["a[0][0]"] --> C2["a[1][0]"] --> C3["a[2][0]"] --> C4["..."] style C1 fill:#ffcdd2 style C2 fill:#ffcdd2 style C3 fill:#ffcdd2 end RowMajor ---|"性能差异约 21.5 倍"| ColMajor ``` --- ## 三、请求分页 ### 基本思想 **请求分页 (Demand Paging)** 是最常用的虚拟存储器实现方式: - 页面只在**被访问时**才调入内存(而非预装入) - 内存不足时,将某些页面**换出**到外存,腾出空间 ### 页表项扩展 在[[14_分页存储管理#四、页表|基本分页]]的基础上,请求分页的页表项增加了**状态位**: | 字段 | 含义 | 说明 | |------|------|------| | **状态位 (Present/Valid)** | 页面是否在内存 | 1=在内存,0=不在内存 | | **访问位 (Reference/Access)** | 页面是否被访问过 | 用于置换算法判断 | | **修改位 (Dirty)** | 页面是否被写过 | Dirty=1 时换出需写回外存 | | **外存地址** | 页面在外存中的位置 | 用于缺页时调入 | ### 缺页中断处理 ```mermaid flowchart TD A["CPU访问虚拟地址"] --> B{"页面在内存中?
(检查状态位)"} B -->|"在内存"| C["正常地址变换,访问数据"] B -->|"不在内存"| D["触发**缺页中断**"] D --> E["保存CPU现场"] E --> F{"外存中存在该页?"} F -->|"不存在"| G["终止进程
Segmentation Fault"] F -->|"存在"| H{"内存有空闲页框?"} H -->|"有空闲"| I["从外存读入该页"] H -->|"无空闲"| J["执行**页面置换算法**
选择牺牲页"] J --> K{"牺牲页Dirty=1?"} K -->|"是"| L["将牺牲页写回外存"] K -->|"否"| M["直接覆盖"] L --> I M --> I I --> N["更新页表:
状态位=1, PPN"] N --> O["刷新TLB"] O --> P["重新执行被中断的指令"] style D fill:#ffcdd2,stroke:#c62828 style J fill:#fff3e0,stroke:#e65100 style G fill:#ffcdd2,stroke:#c62828 ``` --- ## 四、页面置换算法 当内存中没有空闲页框时,需要选择一个页面换出到外存,为新页面腾出空间。不同的选择策略就是**页面置换算法**。 > **示例参数**: 物理内存有 **3 个页框**,页面引用串为: > **7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1** ### 1. OPT (Optimal) 最佳置换算法 **策略**: 换出**将来最长时间不会被使用**的页面。 > OPT 是理论算法,因为无法预知未来的页面访问序列。它用于衡量其他算法的上限。 **执行过程**: | 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 说明 | |---------|-------|-------|-------|-------|------| | **7** | 7 | - | - | 缺页 | 空闲页框,直接装入 | | **0** | 7 | 0 | - | 缺页 | 空闲页框,直接装入 | | **1** | 7 | 0 | 1 | 缺页 | 空闲页框,直接装入 | | **2** | 7 | 0 | **2** | 缺页 | 换出1(最久后才用) | | **0** | 7 | 0 | 2 | 命中 | 0已在内存 | | **3** | **3** | 0 | 2 | 缺页 | 换出7(最久后才用) | | **0** | 3 | 0 | 2 | 命中 | | | **4** | 3 | 0 | **4** | 缺页 | 换出2(最久后才用) | | **2** | 3 | **2** | 4 | 缺页 | 换出0(最久后才用) | | **3** | 3 | 2 | 4 | 命中 | | | **0** | 3 | 2 | **0** | 缺页 | 换出4(最久后才用) | | **3** | 3 | 2 | 0 | 命中 | | | **2** | 3 | 2 | 0 | 命中 | | | **1** | **1** | 2 | 0 | 缺页 | 换出3(最久后才用) | | **2** | 1 | 2 | 0 | 命中 | | | **0** | 1 | 2 | 0 | 命中 | | | **1** | 1 | 2 | 0 | 命中 | | | **7** | 1 | **7** | 0 | 缺页 | 换出2(最久后才用) | | **0** | 1 | 7 | 0 | 命中 | | | **1** | 1 | 7 | 0 | 命中 | | **缺页次数: 9**,缺页率 = 9/20 = **45%** ### 2. FIFO (First-In First-Out) 先进先出 **策略**: 换出**最早进入内存**的页面。 **执行过程**: | 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 淘汰队列 | |---------|-------|-------|-------|-------|---------| | **7** | 7 | - | - | 缺页 | [7] | | **0** | 7 | 0 | - | 缺页 | [7,0] | | **1** | 7 | 0 | 1 | 缺页 | [7,0,1] | | **2** | **2** | 0 | 1 | 缺页 | [0,1,2] 淘汰7 | | **0** | 2 | 0 | 1 | 命中 | [0,1,2] | | **3** | 2 | **3** | 1 | 缺页 | [1,2,3] 淘汰0 | | **0** | 2 | 3 | **0** | 缺页 | [2,3,0] 淘汰1 | | **4** | **4** | 3 | 0 | 缺页 | [3,0,4] 淘汰2 | | **2** | 4 | **2** | 0 | 缺页 | [0,4,2] 淘汰3 | | **3** | 4 | 2 | **3** | 缺页 | [4,2,3] 淘汰0 | | **0** | **0** | 2 | 3 | 缺页 | [2,3,0] 淘汰4 | | **3** | 0 | 2 | 3 | 命中 | [2,3,0] | | **2** | 0 | 2 | 3 | 命中 | [2,3,0] | | **1** | 0 | **1** | 3 | 缺页 | [3,0,1] 淘汰2 | | **2** | 0 | 1 | **2** | 缺页 | [0,1,2] 淘汰3 | | **0** | 0 | 1 | 2 | 命中 | [0,1,2] | | **1** | 0 | 1 | 2 | 命中 | [0,1,2] | | **7** | **7** | 1 | 2 | 缺页 | [1,2,7] 淘汰0 | | **0** | 7 | **0** | 2 | 缺页 | [2,7,0] 淘汰1 | | **1** | 7 | 0 | **1** | 缺页 | [7,0,1] 淘汰2 | **缺页次数: 15**,缺页率 = 15/20 = **75%** > **Belady 异常**: FIFO 算法在某些情况下,增加物理页框数反而会导致缺页率**上升**。例如本例中 4 个页框时缺页次数可能比 3 个页框时更多。这是 FIFO 算法的独特缺陷。 ### 3. LRU (Least Recently Used) 最近最久未使用 **策略**: 换出**最近最长时间没有被访问**的页面。 **实现方式**: - **栈法**: 用一个栈保存页面号,访问某页时将其移到栈顶。栈底就是最久未使用的页面 - **计数器法**: 每个页面记录最后一次访问的时间,淘汰时间值最小的页面 **执行过程**: | 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 说明 | |---------|-------|-------|-------|-------|------| | **7** | 7 | - | - | 缺页 | | | **0** | 7 | 0 | - | 缺页 | | | **1** | 7 | 0 | 1 | 缺页 | | | **2** | 2 | 0 | 1 | 缺页 | 淘汰7(最久未用) | | **0** | 2 | 0 | 1 | 命中 | 0被访问,更新时间戳 | | **3** | 2 | 0 | 3 | 缺页 | 淘汰1(最久未用) | | **0** | 2 | 0 | 3 | 命中 | | | **4** | 4 | 0 | 3 | 缺页 | 淘汰2(最久未用) | | **2** | 4 | 0 | 2 | 缺页 | 淘汰3(最久未用) | | **3** | 4 | 3 | 2 | 缺页 | 淘汰0(最久未用) | | **0** | 0 | 3 | 2 | 缺页 | 淘汰4(最久未用) | | **3** | 0 | 3 | 2 | 命中 | | | **2** | 0 | 3 | 2 | 命中 | | | **1** | 0 | 3 | 1 | 缺页 | 淘汰2(最久未用) | | **2** | 0 | 2 | 1 | 缺页 | 淘汰3(最久未用) | | **0** | 0 | 2 | 1 | 命中 | | | **1** | 0 | 2 | 1 | 命中 | | | **7** | 0 | 2 | 7 | 缺页 | 淘汰1(最久未用) | | **0** | 0 | 2 | 7 | 命中 | | | **1** | 1 | 2 | 7 | 缺页 | 淘汰0(最久未用) | **缺页次数: 12**,缺页率 = 12/20 = **60%** ### 4. Clock (时钟 / 近似LRU) **策略**: 将所有页面组成一个**循环链表**,每页有一个**访问位 R**。淘汰指针从当前位置开始扫描: - R=1:将 R 置 0,指针前移(给第二次机会) - R=0:淘汰该页 ```mermaid flowchart LR subgraph Clock["时钟置换算法 (循环链表)"] direction LR P0["页0
R=1"] P1["页1
R=0"] P2["页2
R=1"] P3["页3
R=0"] end PTR["淘汰指针"] --> P1 P0 --> P1 P1 --> P2 P2 --> P3 P3 --> P0 style P1 fill:#ffcdd2,stroke:#c62828 style PTR fill:#fff3e0,stroke:#e65100 ``` **执行示例**(简化): - 指针指向页1 (R=0) → 淘汰页1,装入新页,指针前移 - 指针指向页2 (R=1) → R置0,指针前移 - 指针指向页3 (R=0) → 淘汰页3 **改进型Clock**: 同时考虑 R 和 D (修改位),优先淘汰 R=0 且 D=0 的页面(不需要写回外存)。 | 优先级 | R | D | 淘汰代价 | |--------|---|---|---------| | 最高 | 0 | 0 | 未访问未修改,直接覆盖 | | | 0 | 1 | 未访问但已修改,需写回 | | | 1 | 0 | 已访问未修改,再给机会 | | 最低 | 1 | 1 | 已访问已修改,最不想淘汰 | ### 5. LFU (Least Frequently Used) 最少使用 **策略**: 换出**访问次数最少**的页面。 - 用计数器记录每个页面的访问次数 - 淘汰计数器值最小的页面 - **缺点**: 某页面过去被频繁访问但现在不再使用,仍难以被淘汰 - **实现开销**: 需要为每个页面维护计数器 ### 算法对比 | 算法 | 策略 | 优点 | 缺点 | 是否有Belady异常 | |------|------|------|------|----------------| | **OPT** | 淘汰将来最久不用的 | 缺页率最低(理论最优) | 无法实现 | 无 | | **FIFO** | 淘汰最早进入的 | 实现简单 | 缺页率高,有Belady异常 | **有** | | **LRU** | 淘汰最近最久未用的 | 接近OPT,效果好 | 实现开销大(需时间戳或栈) | 无 | | **Clock** | 给访问位R=0的淘汰 | LRU的近似,开销小 | 效果略逊于LRU | 无 | | **LFU** | 淘汰访问次数最少的 | 考虑历史频率 | 对访问模式变化响应慢 | 无 | ### 算法效果对比(本例) ``` 页面引用串: 7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1 (3个物理块) ┌──────────┬──────────┬──────────┐ │ 算法 │ 缺页次数 │ 缺页率 │ ├──────────┼──────────┼──────────┤ │ OPT │ 9 │ 45% │ ← 理论最优 │ LRU │ 12 │ 60% │ │ FIFO │ 15 │ 75% │ ← 效果最差 └──────────┴──────────┴──────────┘ ``` --- ## 五、内存分配策略 ### 物理页框分配方式 | 方式 | 说明 | |------|------| | **平均分配** | 将可用页框平均分配给所有进程 | | **按比例分配** | 按进程大小占总需求的比例分配 | | **考虑优先权** | 高优先级进程分配更多页框 | **按比例分配公式**: $$a_i = \frac{s_i}{\sum s_j} \times m$$ 其中 $s_i$ 是进程 $i$ 的页面数,$m$ 是可用页框总数。 ### 置换策略 | 策略 | 说明 | 特点 | |------|------|------| | **固定分配局部置换** | 每个进程固定页框数,只能在自己的页面中置换 | 公平,但可能浪费 | | **可变分配全局置换** | 从全局空闲页框池中分配,可换出任何进程的页面 | 灵活,但可能影响其他进程 | | **可变分配局部置换** | 进程页框数可调,但只能在自己的页面中置换 | 折中方案 | --- ## 六、抖动与工作集 ### 抖动 (Thrashing) 当进程分配到的页框数**不足以**容纳其工作集时,会频繁发生缺页,CPU 大量时间花在页面置换上,利用率急剧下降——这种现象称为**抖动**。 ```mermaid flowchart TD A["进程增多"] --> B["每个进程分到的页框减少"] B --> C["缺页率上升"] C --> D["CPU利用率下降"] D --> E["调度器增加进程数
(以为CPU空闲)"] E --> B style C fill:#ffcdd2,stroke:#c62828 style D fill:#ffcdd2,stroke:#c62828 ``` CPU 利用率随进程数的变化: ``` CPU利用率 ↑ 100%│ ╱‾‾‾‾╲ │ ╱ ╲ │ ╱ ╲ ← 抖动区域 │ ╱ ╲ │ ╱ ╲ │╱ ╲ └──────────────────────────→ 进程数 ↑ 最佳点 ``` ### 工作集 (Working Set) **工作集**的定义:在最近 $\Delta$ 时间内,进程实际访问的页面集合。 $$W(t, \Delta) = \text{在时刻 } t-\Delta \text{ 到 } t \text{ 之间访问的页面集合}$$ **示例**: 页面访问序列: ... 2, 6, 1, 5, 7, 7, 7, 2, ... 设 $\Delta = 5$(最近 5 次访问),当前访问页面 2: - 最近 5 次访问: {5, 7, 7, 7, 2} - 工作集 = {2, 5, 7},大小 = 3 **工作集策略**: 为每个进程分配**不少于其工作集大小**的页框数,就可以避免抖动。 ### 工作集模型的应用 ```mermaid flowchart TD A["监控每个进程的工作集"] --> B{"总工作集 ≤ 物理页框数?"} B -->|"是"| C["正常运行,不会抖动"] B -->|"否"| D["可能发生抖动"] D --> E["挂起某些进程
释放页框"] E --> B style D fill:#ffcdd2,stroke:#c62828 style C fill:#c8e6c9,stroke:#2e7d32 ``` --- ## 七、请求分段 ### 基本思想 **请求分段**是虚拟存储器在[[15_段式存储管理|分段]]基础上的实现: - 段不需要全部装入内存 - 访问到不在内存的段时,触发**缺段中断**,从外存调入 ### 与请求分页的对比 | 对比项 | 请求分页 | 请求分段 | |--------|---------|---------| | 调入/调出单位 | 页面(固定大小) | 段(可变大小) | | 碎片 | 内碎片 | 外碎片 | | 共享 | 以页为粒度 | 以段为粒度 | | 中断类型 | 缺页中断 | 缺段中断 | | 实现复杂度 | 较简单 | 较复杂 | --- ## 八、完整计算示例 ### 综合例题 > 某系统采用请求分页存储管理,页面大小为 4KB,物理内存有 4 个页框。某进程的页面访问序列为:**0, 1, 4, 2, 0, 2, 6, 5, 1, 2, 3, 2, 1, 2, 6, 5, 2, 1, 3, 6**。分别用 FIFO 和 LRU 算法计算缺页次数。 **FIFO 算法**: | 访问 | 页框0 | 页框1 | 页框2 | 页框3 | 缺页? | |------|-------|-------|-------|-------|-------| | 0 | 0 | - | - | - | 缺页 | | 1 | 0 | 1 | - | - | 缺页 | | 4 | 0 | 1 | 4 | - | 缺页 | | 2 | 0 | 1 | 4 | 2 | 缺页 | | 0 | 0 | 1 | 4 | 2 | 命中 | | 2 | 0 | 1 | 4 | 2 | 命中 | | 6 | 6 | 1 | 4 | 2 | 缺页(淘汰0) | | 5 | 6 | 5 | 4 | 2 | 缺页(淘汰1) | | 1 | 6 | 5 | 1 | 2 | 缺页(淘汰4) | | 2 | 6 | 5 | 1 | 2 | 命中 | | 3 | 6 | 5 | 1 | 3 | 缺页(淘汰2) | | 2 | 2 | 5 | 1 | 3 | 缺页(淘汰6) | | 1 | 2 | 5 | 1 | 3 | 命中 | | 2 | 2 | 5 | 1 | 3 | 命中 | | 6 | 2 | 6 | 1 | 3 | 缺页(淘汰5) | | 5 | 2 | 6 | 5 | 3 | 缺页(淘汰1) | | 2 | 2 | 6 | 5 | 3 | 命中 | | 1 | 2 | 6 | 5 | 1 | 缺页(淘汰3) | | 3 | 3 | 6 | 5 | 1 | 缺页(淘汰2) | | 6 | 3 | 6 | 5 | 1 | 命中 | **FIFO 缺页次数: 14** **LRU 算法**: | 访问 | 页框0 | 页框1 | 页框2 | 页框3 | 缺页? | |------|-------|-------|-------|-------|-------| | 0 | 0 | - | - | - | 缺页 | | 1 | 0 | 1 | - | - | 缺页 | | 4 | 0 | 1 | 4 | - | 缺页 | | 2 | 0 | 1 | 4 | 2 | 缺页 | | 0 | 0 | 1 | 4 | 2 | 命中 | | 2 | 0 | 1 | 4 | 2 | 命中 | | 6 | 0 | 1 | 6 | 2 | 缺页(淘汰4) | | 5 | 0 | 5 | 6 | 2 | 缺页(淘汰1) | | 1 | 1 | 5 | 6 | 2 | 缺页(淘汰0) | | 2 | 1 | 5 | 6 | 2 | 命中 | | 3 | 1 | 5 | 3 | 2 | 缺页(淘汰6) | | 2 | 1 | 5 | 3 | 2 | 命中 | | 1 | 1 | 5 | 3 | 2 | 命中 | | 2 | 1 | 5 | 3 | 2 | 命中 | | 6 | 1 | 5 | 3 | 6 | 缺页(淘汰2) | | 5 | 1 | 5 | 3 | 6 | 命中 | | 2 | 2 | 5 | 3 | 6 | 缺页(淘汰1) | | 1 | 2 | 1 | 3 | 6 | 缺页(淘汰5) | | 3 | 2 | 1 | 3 | 6 | 命中 | | 6 | 2 | 1 | 3 | 6 | 命中 | **LRU 缺页次数: 12** --- ## 九、小结 ```mermaid mindmap root((虚拟存储器)) 基本思想 虚拟空间>物理内存 部分装入 按需调入调出 局部性原理 时间局部性 空间局部性 行优先vs列优先 请求分页 页表扩展状态位 缺页中断处理 页面置换 置换算法 OPT理论最优 FIFO简单但有Belady异常 LRU效果好但开销大 Clock近似LRU LFU最少使用 内存分配 平均/按比例/优先权 固定/可变分配 局部/全局置换 抖动与工作集 频繁缺页=抖动 工作集=近期访问页面集 帧数≥工作集大小 ``` --- ## 思考题 1. **概念理解**: 为什么说虚拟存储器的基础是局部性原理?如果没有局部性,虚拟存储器还能工作吗? 2. **算法对比**: 对于页面引用串 1,2,3,4,1,2,5,1,2,3,4,5(3个物理块),分别计算 OPT、FIFO、LRU 的缺页次数。是否存在Belady异常? 3. **工作集**: 某进程页面访问序列为 2,6,1,5,7,7,7,2,5,1,3,1,5,7,2,5,设 $\Delta=5$,求各时刻的工作集大小。 4. **抖动分析**: 某系统物理内存可容纳 10 个工作集页面。目前有 5 个进程,工作集大小分别为 {3,2,2,1,3}。此时再增加一个工作集大小为 3 的进程,系统是否会发生抖动? 5. **综合题**: 在请求分页系统中,页面大小 4KB,页表项 4 字节,虚拟地址 32 位,物理地址 32 位。 - 一级页表有多大? - 如果采用二级页表,页目录和页表各多大? - 如果 TLB 命中率 95%,TLB 访问 10ns,内存访问 100ns,有效访问时间是多少? --- ## 关联笔记 - [[14_分页存储管理]] — 分页基础、页表结构、地址变换 - [[15_段式存储管理]] — 分段思想、段页式 - [[11_处理机调度]] — 进程调度与抖动的关系 --- **上一讲**: [[15_段式存储管理]] **目录**: [[00_课程导航]]