虚拟内存与页面置换
虚拟内存(Virtual Memory)让程序用比物理内存更大的地址空间——不常用的数据放在磁盘上,需要时再换进来,就像大学宿舍放不下的行李寄存到仓库
8GB 内存能跑需要 16GB 的游戏吗?
可以——只要你不在乎一点卡顿。
虚拟内存(Virtual Memory) 让物理内存的容量不再是硬限制。不常用的数据被挪到磁盘上(Swap),需要时再换回来。
🏫 类比:宿舍行李箱 你的宿舍很小(物理内存),但你有 4 个行李箱(数据和程序)。你只把最常用的东西放在宿舍里——当季的衣服、课本、笔记本。换季的衣服和旧教材存在家里的仓库(磁盘)。
想穿羽绒服了(需要访问不常用的数据)→ 回家取一趟(从磁盘换入)。 宿舍放不下了 → 把不穿的衣服送回仓库(换出到磁盘)。
虚拟内存的工作原理
缺页异常(Page Fault)
当程序访问的页不在物理内存中时,MMU 触发缺页异常(Page Fault):
程序访问虚拟地址 0x7f001234
│
▼
MMU 查页表 → PTE.Present = 0(此页不在内存)
│
▼
CPU 触发缺页异常(Page Fault #14)
│
▼
操作系统缺页异常处理程序:
① 找一个空闲的物理页框
② 从磁盘读取需要的数据到这个页框
③ 更新页表(写入物理页框号,Present=1)
④ 重新执行引发异常的指令
│
▼
程序继续执行(不知道刚才发生了 Page Fault)
; Page Fault 的触发——在 CR2 中记录访问的地址
page_fault_handler:
mov rax, cr2 ; CR2 = 引发缺页的虚拟地址
; 查磁盘上对应的数据位置
; 分配物理页框
; 读磁盘 → 物理页框
; 更新页表
iretq ; 返回用户程序重试
💡 缺页异常的处理非常昂贵——读一次磁盘大约 5-10ms,而 CPU 每秒能执行几十亿条指令。所以缺页越少,程序越快。
页的三种状态
┌─────────────────────────────┐
│ 虚拟页的状态 │
├─────────────┬───────────────┤
│ 在物理内存 │ 在磁盘上 │
│ Present=1 │ Present=0 │
│ 可以直接访问 │ 访问会触发缺页 │
├─────────────┴───────────────┤
│ 不存在 │
│ 未分配,访问会触发段错误 │
└─────────────────────────────┘
页面置换算法
物理内存满了,需要一个”牺牲页”换出去——选哪个?这就是页面置换算法。
1. FIFO(先进先出)
最早进入内存的页先被换出。
请求页序列:1, 2, 3, 4, 1, 2, 5
内存(3 页):
1 → [1]
2 → [1, 2]
3 → [1, 2, 3]
4 → [2, 3, 4] ← 1 被换出
1 → [3, 4, 1] ← 2 被换出
...
问题:Belady 异常——增加页数反而导致更多缺页(FIFO 独有的反直觉问题)。
2. LRU(最近最久未使用)
淘汰最长时间没被访问的页——基于”局部性原理”。
请求页序列:1, 2, 3, 4, 1, 2, 5
内存(3 页):
1 → [1]
2 → [1, 2]
3 → [1, 2, 3]
4 → [2, 3, 4] ← 1 被换出(1 是最近最久没用的)
1 → [3, 4, 1] ← 2 被换出
2 → [4, 1, 2] ← 3 被换出
5 → [1, 2, 5] ← 4 被换出
缺点:完美 LRU 需要记录每页的访问时间或维护一个按访问时间排序的链表——硬件实现成本太高。
💡 实际系统用 近似 LRU——比如利用页表项的 Accessed 位,定期扫描清除 Accessed 位,保留还为 1 的是最近用过的。
3. 时钟算法(Clock / Second Chance)
LRU 的实用近似——把页排成环形,用”指针”和”使用位”决定换出谁。
┌─→ P1(使用位=1 → 置0,继续)
│ P2(使用位=0 → 换出它!)
│ P3(使用位=1 → 置0,继续)
│ P4(使用位=0 → OK)
└─── 指针循环扫描
流程:
- 检查指针指向的页的使用位(Accessed)
- 如果使用位 = 1 → 置为 0,指针移向下一个
- 如果使用位 = 0 → 换出这一页
- 如果所有使用位都是 1→扫一圈全部清 0→换出扫到的第一页
💡 时钟算法是操作系统中应用最广的置换算法——Linux、Windows 都使用其变种。
4. LFU(最不经常使用)
淘汰访问次数最少的页。
问题:一个页在程序启动时被大量访问(使用次数高),之后再也不用了——按 LFU 它就永远留在内存中。
算法对比
| 算法 | 核心思想 | 实现难度 | 性能 |
|---|---|---|---|
| FIFO | 按进入顺序 | 极简 | 差(Belady 异常) |
| LRU | 最近没用的大概率以后也不用 | 难 | 优(理论最优近似) |
| 时钟 | LRU 的近似,用使用位 | 中 | 良 |
| LFU | 按使用频率 | 中 | 一般 |
页面分配策略
物理内存有限,给每个进程分配多少页框?
| 策略 | 分配方式 | 优点 | 缺点 |
|---|---|---|---|
| 固定分配 | 每个进程固定页框数 | 公平 | 无法应对不同需求 |
| 可变分配 | 根据缺页率动态调整 | 灵活 | 实现复杂 |
抖振(Thrashing)
抖振是虚拟内存最严重的性能问题——分配的物理页太少,导致频繁缺页,CPU 大部分时间在处理缺页而不是真正执行。
物理内存不够 → 频繁缺页 → 频繁 I/O 读盘
↓
CPU 利用率降低 → OS 认为 CPU 空闲 → 调入更多进程
↓
每个进程分到的页框更少 → 缺页更频繁 → ...
↓
系统崩溃(几乎不干活)
💡 抖振的检测:当 CPU 利用率下降而磁盘 I/O 急剧增加时,系统很可能在抖振。
Thrashing 的解决方案
工作集模型(Working Set)
一个进程在一段时间内实际访问的页集合称为它的工作集。只要分配给进程的页框数 >= 工作集大小,缺页率就会低。
时间轴 →
访问的页: [1, 2, 3, 1, 2, 4, 5, 1, 2, 3, ...]
└─────────┘
工作集(最近 5 次访问的页)
= {1, 2, 3, 4, 5}
缺页频率控制(Page Fault Frequency, PFF)
缺页率太高 → 给进程多分配页框
缺页率太低 → 从进程回收多余页框
小结
| 概念 | 要点 |
|---|---|
| 虚拟内存 | 用磁盘空间扩展”虚拟”的物理内存 |
| 缺页异常 | 访问不在内存中的页时触发,OS 从磁盘加载 |
| 页面置换 | 内存满了要腾空间,选择换出哪一页 |
| LRU | 换出最近最久未用的页(理论最优近似) |
| 时钟算法 | LRU 的实用实现 |
| 抖振 | 页框太少→频繁缺页→系统几乎停摆 |
为什么先学这个? 虚拟内存解释了为什么你的电脑可以跑比物理内存更大的程序。但你可能会好奇——页表查询需要多次内存访问,怎么加速?答案是TLB(Translation Lookaside Buffer)。