虚拟内存

← 内存管理 | ← 操作系统知识地图


为什么需要虚拟内存

程序要运行,传统做法是把整个进程全部装入内存。但很多程序实际运行时只会用到其中一小部分代码和数据(比如错误处理分支、冷门功能),全部装入浪费物理内存,也限制了多道程序并发数。

虚拟内存的核心思想:只把进程当前真正需要的部分装入内存,其余仍留外存,需要时再调入。

这带来三个直接好处:

  1. 进程逻辑地址空间可以超过物理内存大小
  2. 物理内存中能同时容纳更多进程
  3. 每个进程的装入量减少,换入换出开销更小

局部性原理

虚拟内存能成立的前提是程序访问内存具有局部性——它不会把整个地址空间均匀扫一遍。

类型含义典型场景
时间局部性刚访问过的数据/指令,不久后很可能再次被访问循环体、频繁使用的变量
空间局部性某地址被访问后,相邻地址很可能也被访问顺序执行的指令、数组遍历

局部性保证了按需调页在多数场景下有效。但如果程序访问模式是随机跳跃(如哈希表全表扫描),缺页会极为频繁,虚拟内存反而拖累性能——这就是”按需调入不适合线性/随机搜索”的本质原因。


页表原理与地址变换 → 页式管理

页表项在虚拟内存中的扩展

虚拟内存下并非所有页都在内存中,页表项除了基本字段外,还需额外记录:

字段作用
存在位(有效位)该页是否已在内存中
外存地址若不在内存,记录它在外存(文件区/对换区)的位置
访问位该页近期是否被访问过(置换判断依据)
修改位(脏位)该页内容是否被修改过(换出时决定是否回写)

页表项基本字段(页框号、保护位等)见 页式管理·页表

缺页中断

CPU 访存,MMU 查页表——基本流程同 页式管理·地址变换,唯一的区别在步骤 2:当存在位 = 0,页不在内存,触发缺页中断(内中断,指令执行期间发生):

1. CPU 访存,MMU 查页表发现该页不在内存
2. 硬件触发缺页中断,陷入操作系统
3. OS 查 PCB 找到该页在外存中的位置
4. 若内存有空闲页框 → 直接装入
   若无空闲页框   → 按置换算法选一页换出
5. 被换出页若修改过(脏页)→ 先回写外存
6. 将所需页面装入目标页框,更新页表
7. 重新执行刚才因缺页而中断的那条指令

TLB 原理 → 页式管理·TLB


多级页表原理 → 页式管理·两级页表

页框分配

虚拟内存不是无限制占用物理内存,每个进程只分配一定数量的页框。

最小页框数与驻留集

  • 最小页框数:进程能正常运行所需的最少页框数。由指令集架构决定——至少需要覆盖一条指令执行过程中可能访问的所有页面(指令本身、源操作数、目的操作数)。
  • 驻留集:某一时刻实际分配给进程、驻留在内存中的页面集合。对应的页框数量就是驻留集大小。

驻留集大小的影响

驻留集大小影响
太小频繁缺页,刚调入的页马上又被换出,CPU 大量时间耗费在调页
太大单个进程缺页少,但挤占其他进程的可用页框,降低并发度

内存分配与置换策略

策略组合由两个维度决定:页框数是否固定、置换范围是局部还是全局。

策略页框数置换范围特点
固定分配局部置换固定本进程隔离性强,管理简单;初始分配不当则频繁缺页
可变分配全局置换可变所有进程整体利用率高;进程间互相影响大
可变分配局部置换可变本进程折中方案:动态调整 + 局部控制

固定分配局部置换

进程启动时分配固定数量页框,此后不变。缺页时只能从本进程自己的页框中选一页换出。问题是如果没估准进程的实际内存需求,分配不足时进程会持续缺页。

可变分配全局置换

系统根据运行情况动态增减进程页框数。缺页时,换出目标可以来自任何进程,回收的空闲页框也可以重新分配给缺页多的进程。缺点是进程间干扰:一个缺页频繁的进程可能挤占其他进程的页框。

可变分配局部置换

页框数可以动态调整,但每次缺页置换时只从本进程驻留集中选页换出。它试图兼顾灵活性与隔离性。

换入来源:文件区与对换区

缺页时从外存调入的页面来源分两种:

来源存放内容调入场景
文件区原始程序文件、只读数据该页未被修改过,可直接从可执行文件重新读入
对换区曾经装入内存后被换出的页面运行中被修改过的页换出后存于此处,缺页时从对换区调回

本质区别:文件区的页”丢了还能从原文件拿”,对换区的页”丢了就没法恢复,必须保存”。


页引用字符串

页面置换算法的输入是什么?答案是进程运行期间依次访问的逻辑页号序列——页引用字符串(Reference String)。

比如某进程先后访问了第 0 页、第 2 页、第 1 页……:

每个数字是一个逻辑页号,不是物理地址,也不是字节地址。

计算题的标准化套路:给定引用字符串 + 可用页框数,逐页模拟置换算法的行为,统计缺页次数(或命中次数)。

引用字符串的来源:

  • 由进程的实际访存地址序列 页大小,取商(页号)得到
  • 连续重复访问同一页号是否合并为一个,看题目说明

引用字符串是页号序列——这是最容易搞混的点:把地址直接当页号算,结果全是错的。


页面置换算法

发生缺页且无空闲页框时,必须选一页换出。选择策略就是页面置换算法。

OPT 最佳置换算法

规则:淘汰未来最长时间内不再被访问的页面。

  • 缺页率最低,是理论上的最优解
  • 无法实现:OS 不可能预知未来访问序列
  • 用途:作为评价其他算法的比较基准

FIFO 先进先出置换算法

规则:淘汰最早进入内存的页面。

  • 实现简单:维护一个 FIFO 队列即可
  • 只看新旧不看频率,可能把经常使用的”老页”换出去
  • Belady 异常:分配的页框数增多,缺页次数反而增加

Belady 异常示例:访问序列 3,2,1,0,3,2,4,3,2,1,0,4,3 个页框时缺页 9 次,4 个页框时反而缺页 10 次。FIFO 是典型的会出现 Belady 异常的算法。

LRU 最近最久未使用

规则:淘汰最近最长时间未被访问过的页面。

  • 基于局部性原理:很久没用的页接下来大概率也不会用
  • 性能接近 OPT
  • 实现代价高:需要记录每次访问的时间戳或维护访问栈

LRU 的硬件实现方式:

方式做法开销
计数器每次访存 CPU 将递增的时钟值写入页表项,置换时找最小值每次访存都要写内存
每次访问将该页号移到栈顶,置换时取栈底页号每次访存都要调整栈(指针操作较多)

CLOCK 时钟置换算法(NRU 近似)

规则:驻留集页面排成环形链表,指针循环扫描,利用访问位判断。

1. 指针指向某页
2. 若访问位 = 0 → 换出该页
3. 若访问位 = 1 → 清 0,跳过,指针继续移动

相当于给访问过的页”第二次机会”——这轮被扫到但近期用过,先放过;下一轮再扫到如果仍未被访问,就淘汰。

  • 比 LRU 实现简单得多(只用一个硬件位)
  • 效果明显好于 FIFO
  • 实际系统中非常常用

改进 CLOCK 算法

在访问位之外纳入修改位,因为换出脏页要回写磁盘,代价远大于换出干净页。

分为四档优先级:

优先级(访问位, 修改位)含义换出策略
最高换出(0, 0)最近未用,未修改首选淘汰,无需回写
次选(0, 1)最近未用,已修改可淘汰,但需回写
较留(1, 0)最近用过,未修改暂缓
最低换出(1, 1)最近用过,已修改尽量保留

扫描逻辑:先找 (0, 0),找不到再找 (0, 1),同时沿途把访问位清 0——给所有页面一次”降级”的机会。


抖动与工作集

抖动

进程分配的页框数太少,活跃页面装不下驻留集,导致页面刚调入就被换出、马上又调入——系统大量时间耗费在调页 I/O 上,CPU 利用率反而暴跌。

抖动发生链路:
驻留集 < 活跃页面 → 频繁缺页 → 大量 I/O 等待 → CPU 空闲
→ 调度器误判"CPU 闲,加进程" → 进程更多 → 缺页更严重 → 死循环

工作集

工作集:某段时间窗口 内,进程频繁访问的页面集合。可以理解为进程在当前阶段”真正离不开”的那批页。

关系结果
驻留集 工作集缺页率低,正常运行
驻留集 工作集频繁缺页,可能抖动

工作集模型常被用于指导驻留集大小的动态调整:操作系统监控每个进程的工作集变化,据此增减其分配的页框数。这也引出了工作集置换算法——换出不在当前工作集中的页面。


写时复制(Copy-on-Write)

fork() 创建子进程时,传统做法是把父进程的整个地址空间完整复制一份。但大多数情况下,fork() 之后紧跟 exec(),刚复制完的页面马上就被覆盖——复制完全白做。

Copy-on-Write 策略

  1. fork() 时父子进程共享同一套物理页面,页表项标记为只读
  2. 任意一方试图写入某页 → 触发页保护异常
  3. OS 捕获异常,此时才真正复制该页,并为写入方恢复可写权限
  4. 没被写的页面继续共享,永不复制

这样大量节省了 fork() 的内存开销和时间。


页框回收

系统不会等到一个空闲页框都没有才行动。当空闲页框数低于某阈值(低水位线),就启动后台回收。

回收时区分:

页面类型回收处理
干净页(修改位=0)可直接回收;若来自文件映射且未修改,需要时再从文件读入
脏页(修改位=1)必须先回写磁盘/对换区,再回收页框

内存映射(Memory-Mapped I/O)

操作系统将文件或设备的一段内容映射到进程的虚拟地址空间,进程可以像访问内存一样访问文件内容。

优势说明
简化文件访问不必显式 read/write,指针操作即可
按需调页访问到哪页就调入哪页(demand paging)
多进程共享多个进程把同一文件映射到各自地址空间,底层共享同一物理页

内存映射本质上是利用虚拟地址空间的映射能力,让不同访问者围绕同一份实际物理内容建立联系——与共享内存原理相通。