页面置换算法与实战机制:从 LRU 到内存映射文件

页面置换算法与实战机制:从 LRU 到内存映射文件

引言

前面我们建立了虚拟内存的框架:程序不需要全部装入内存,缺页时动态从磁盘装入。但物理页框的数量是有限的------当所有页框都被占满、又发生了新的缺页中断时,操作系统必须做出选择:把哪个正在内存中的页换出去,为即将装入的页腾出位置?

这就是页面置换算法(Page Replacement Algorithm)要解决的问题。如果选择得当------淘汰一个以后很长时间不会再被访问的页------系统的缺页率就低、性能就好。如果选择不当------淘汰掉一个马上又要被访问的页------刚换出又得换入,系统就会频繁缺页,性能崩溃。

📌 核心要点

  • OPT(最优置换)算法作为不可实现的理论基准,提供缺页率下界;LRU 是理论上最接近 OPT 且可实现的算法,但硬件的完全 LRU 实现开销太大。
  • CLOCK(NRU)和改进型 CLOCK 是操作系统中实际使用的页面置换算法------用一个访问位 A 和一个修改位 M 近似模拟 LRU 的淘汰顺序,硬件实现极其简单。
  • Belady 异常只发生在 FIFO 算法中:增加分配给进程的物理页框数,缺页次数反而可能增加。LRU 和 OPT 属于"栈算法"(Stack Algorithm),不受此影响。
  • 工作集(Working Set)模型用窗口参数 Δ 来定义"最近 Δ 次访问中涉及的页面集合";当系统中所有进程的工作集总和超过物理内存时,系统进入抖动(Thrashing)状态。

页面置换算法(一)------从理论到实践

为方便说明,我们用以下经典题型中的访问串来演示各算法:

假设系统为进程分配了 3 个物理页框,页访问串为:

7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1

最优置换(OPT / Belady's Optimal Algorithm)

策略:淘汰在未来最长时间内不会再被访问的页面。

对于访问串 7 0 1 2 ...:初始连续装入 7、0、1(三次缺页)。第四次访问 2 时,三个页框都满了,需要置换。此时查看 7、0、1 在未来哪一页最晚才会被用到------7 在第 18 次访问才会重新出现,0 在第 5 次就会出现,1 在第 14 次重新出现。显然该淘汰 7。

OPT 的缺页次数是这个访问串在给定页框数下能达到的理论最小值。对于上述序列和 3 个页框,OPT 产生 9 次缺页(含最初的 3 次强制缺页)。

OPT 不可实现 ,因为它要求预知未来的访问序列------这在实际系统中不可能做到。但它的价值巨大:它提供了一个下界(Lower Bound),让其他算法的缺页数可以与之比较------距 OPT 越近,算法越好。它也是证明"一个算法是否出现 Belady 异常"的参照。

先进先出(FIFO)

策略:淘汰最早进入内存的页面------在内存中驻留时间最长的页先出去。可以用一个 FIFO 队列实现。

对于上述序列和 3 个页框:

时刻 访问 页框 0 页框 1 页框 2 缺页?
1 7 7 - -
2 0 7 0 -
3 1 7 0 1
4 2 2 0 1 ✓ (淘汰 7)
... ... ... ... ... ...

FIFO 在这个序列上产生 15 次缺页------远多于 OPT 的 9 次。

FIFO 的优点是实现极其简单:一个队列搞定。缺点是与程序的实际访问模式毫无关联------一个进程可能在运行初期装入了一个初始化函数所在的页面,从此再也不访问了,但 FIFO 会把它保留到最后,因为它"进来得早"。

最近最久未使用(LRU, Least Recently Used)

策略:淘汰最近最长时间未被访问的页面。直觉上这模拟了"最优"------如果过去很久没用,未来大概也不会马上用到。

LRU 有两种硬件实现方案:

实现方案一:计数器(Counter)

CPU 内部为每个页关联一个时间戳计数器。每发生一次内存访问,当前时钟值就写入被访问页面关联的计数器中。需要置换时,遍历所有页,选择计数器值最小的那个(即最久未被访问的)。

缺点:每次内存访问都要更新计数器,置换时需要遍历全表比较------开销不可忽略。而且计数器会溢出,需要定期重置。

实现方案二:栈(Stack)

用一个特殊的栈结构:每访问一个页,就把该页号从栈中弹出(如果它在栈中)再压入栈顶。栈顶始终是"最近刚被访问"的页,栈底是"最久未被访问"的页。需要置换时,直接淘汰栈底的页。

每次访问需要更新栈(查找和移动页号),开销同样不小。但栈方案比计数器更直观------不需要做"比较计数器值"这个步骤。

对于上述序列和 3 个页框,LRU 产生 12 次缺页------多于 OPT 的 9 次,但远好于 FIFO 的 15 次。

LRU 在数学上与 OPT 共享一个重要特性:它们都属于栈算法 (Stack Algorithm)。栈算法的定义是:对于同样的访问序列,给 N 个页框时的内存页面集合总是包含在给 N+1 个页框时的内存页面集合之中。这个特性的直接推论是:它们不会出现 Belady 异常------多给页框只会让缺页更少或不变,绝不会更多。FIFO 不属于栈算法,所以它有可能出现 Belady 异常。


页面置换算法(二)------CLOCK 系列的工程智慧

完全的 LRU 需要每次访问都更新硬件数据结构------计数器或栈。这在 1970 年代的硬件上开销过于昂贵。实际操作系统需要一种近似 LRU 但硬件开销极小的方案。

简单 CLOCK(NRU, Not Recently Used)

CLOCK 算法也被称为最近未使用算法 (NRU),核心操作依赖一个硬件支持的访问位(Access Bit / Reference Bit)。每当一个页面被访问时,硬件自动将该页的访问位设为 1。操作系统可以读取和清零这个位。

算法用一个循环指针(像时钟的指针一样绕圈),每次需要置换时:

  1. 从指针当前位置开始扫描。
  2. 如果当前页的访问位 = 0:选择该页淘汰。
  3. 如果当前页的访问位 = 1:将访问位清零("再给一次机会"),指针继续前进。
  4. 重复步骤 2-3,直到找到一个访问位 = 0 的页。

如果所有页的访问位都是 1(全部被访问过),指针会绕一整圈,把所有访问位清零,然后回到最初的位置------这次的访问位已经是 0 了,淘汰之。

CLOCK 的行为近似于 LRU:最近被访问过的页(访问位 = 1)获得了一次"豁免",只有"最近未被访问过"的页(访问位在被清零后仍为 0)才会被淘汰。而这个"豁免"是通过一个 1-bit 的访问位实现的------硬件成本几乎为零。

改进型 CLOCK(Enhanced CLOCK / Second Chance with Modify Bit)

简单 CLOCK 只看"是否被访问过",但没有考虑"是否被修改过"。如果被淘汰的页是脏页(M=1,有写入),就需要先写回磁盘------增加一次磁盘 I/O。如果淘汰的是干净页(M=0),可以直接覆盖------省掉一次写回。

改进型 CLOCK 同时考虑访问位 A修改位 M,将页面分为四个优先级(优先级越高,越优先被淘汰):

优先级 (A, M) 含义 淘汰策略
第一优先(最优淘汰) (0, 0) 最近未访问、未被修改 直接淘汰,零额外 I/O
第二优先 (0, 1) 最近未访问、但被修改过 淘汰前需要写回磁盘(一次 I/O)
第三优先 (1, 0) 最近被访问、但未被修改 可能很快又会被访问,推迟淘汰
第四优先(最后淘汰) (1, 1) 最近被访问、且被修改过 最不该淘汰的页

算法流程:

  1. 从指针当前位置开始,第一轮扫描:寻找 (0, 0) 的页。找到即淘汰,不修改任何访问位。
  2. 若第一轮没找到,第二轮扫描:寻找 (0, 1) 的页。扫描过程中将经过的页的访问位清零。找到后淘汰,但需要先写回磁盘。
  3. 若前两轮都没找到,所有页的访问位都已被清零(M 保持原值),此时重新执行第一、第二轮的逻辑------最终一定能淘汰一个页。

改进型 CLOCK 的工程价值在于:用一个 2-bit 的 (A, M) 组合就实现了对 LRU 的良好近似 同时 把"写回磁盘"的次数最小化。从 Linux 2.6 内核的页面回收算法,到现代 BSD 系统的页面置换,改进型 CLOCK 的变体广泛存在于真实的操作系统内核中。

各算法缺页率直观对比(以上述 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% 最接近 OPT 的可实现算法
CLOCK 约 13 约 65% 取决于顺序,接近 LRU
FIFO 15 75% 简单但与访问模式无关

CHART: 页面置换算法缺页次数对比 → charts/02-page-replacement-comparison.svg


Belady 异常------FIFO 才有的反直觉现象

Belady 异常是 408 统考中一定会出现的考点。它的定义是:对于 FIFO 页面置换算法,增加分配给进程的物理页框数,缺页次数反而可能增加

直觉上,更多的页框意味着更多的页可以同时驻留在内存中,缺页应该更少。这个直觉对 LRU 和 OPT 是正确的(因为它们属于栈算法),但对 FIFO 不成立。

经典例题 :对于页访问串 1 2 3 4 1 2 5 1 2 3 4 5

  • 分配 3 个页框 → FIFO 缺页 9 次
  • 分配 4 个页框 → FIFO 缺页 10 次(多了!)

这就是 Belady 异常------多给了一个页框,缺页反而多了一次。(具体的手工推演过程是 408 真题的经典题型,建议读者自行画一遍,体会 FIFO 的非栈特性。)

栈算法(如 LRU、OPT)满足包含性:N 个页框时的内存页面集合 ⊆ N+1 个页框时的内存页面集合。因此增加页框数不会引入新的缺页------这就是为什么 LRU 和 OPT 不受 Belady 异常影响。FIFO 不满足包含性------加了一个页框之后,原有的一些页面被淘汰的时机发生了变化,有时反而更不利。

💡 实践建议:408 考题中判断"某算法是否会出现 Belady 异常"的捷径:看它是否为栈算法。LRU 和 OPT 一定不会出现;FIFO 和其他非栈算法可能出现。考试中如果出现"增大页框数,缺页反而增加"的描述,直接指向 FIFO。


页面分配策略------每进程该拿多少页框

在多道程序环境中,物理页框需要在多个进程之间分配。这是一个全局决策:给进程 A 多些页框意味着进程 B 必须少些------而每个进程的缺页率都受分配数量的影响。

三个维度的组合

固定分配 vs 可变分配

  • 固定分配:每个进程在生命周期中被分配固定数量的物理页框,不变。
  • 可变分配:进程的页框数可以根据其运行情况和系统负载动态调整。

局部置换 vs 全局置换

  • 局部置换:发生缺页时,只能从该进程自己的页面中选择淘汰页------不影响其他进程。
  • 全局置换:发生缺页时,可以从系统中所有进程的页面中选择淘汰页------可能会减少其他进程的页框数。

两者的交叉组合产生三种可行方案:

方案 分配 置换 特点
固定分配 + 局部置换 固定 局部 简单但缺少灵活性。若分配过少则缺页频繁;若分配过多则浪费。
可变分配 + 全局置换 可变 全局 系统中各进程的页框数随缺页率动态调整。实现最灵活,但可能引发进程间的"无辜牵连"。
可变分配 + 局部置换 可变 局部 每个进程的页框数独立调整,置换也独立。是 Linux 等现代 OS 的常用策略。

⚠️ 固定分配 + 全局置换是无意义的组合------如果页框固定不变,整个系统的页框总数被锁死,"全局"置换就失去了意义:你无法从进程 A 拿一个页框给进程 B 而不改变 A 的页框数。


工作集模型与抖动------当大家都在抢内存

工作集(Working Set)

1968 年,Peter Denning 提出了工作集模型。定义:进程在最近 Δ 次(或 Δ 时间内)的页面访问中所涉及的页面集合,称为该进程的工作集 W(t, Δ)。其中 Δ 是工作集窗口(Working Set Window)。

工作集的大小反映了进程在当前的"活跃内存需求"。如果进程当前的工作集可以完全驻留在物理内存中,该进程处于正常状态。如果系统分配给进程的页框数小于它的工作集大小,该进程就会频繁缺页。

工作集模型的一个直接应用:操作系统可以监控每个进程的工作集大小,据此动态调整页框分配。如果一个进程的工作集变大,给它更多页框;如果工作集缩小,回收多余页框给其他进程。

抖动(Thrashing)

抖动是虚拟内存系统中最严重的性能灾难。它的成因链是:

  1. 系统中的并发进程太多,每个进程分到的页框太少。
  2. 每个进程的工作集都无法完全驻留在内存中 → 缺页率飙升。
  3. 每个缺页都触发磁盘 I/O → 进程排队等待 I/O。
  4. CPU 利用率骤降(因为所有进程都在等磁盘)。
  5. 操作系统调度器检测到 CPU 利用率低,以为并发度不够 → 再调入更多进程
  6. 回到第 1 步,循环恶化。

在这个恶性循环的终点,系统进入抖动状态:大量时间花在页面换入换出上,CPU 实际有效的计算时间趋于零。

PFF(Page Fault Frequency)------缺页率驱动的自适应分配

防治抖动的一种有效策略是监控每个进程的缺页频率(Page Fault Frequency):

  • 如果进程的缺页率高于设定的上界(说明页框太少、工作集装不下)→ 给它分配更多页框。
  • 如果进程的缺页率低于设定的下界(说明页框太多、浪费)→ 回收一些页框分给更需要它们的进程。
  • 如果系统中所有进程的缺页率都高、且没有空闲页框可分配 → 系统进入抖动,应该挂起某些进程(将其交换出内存)。

页框回收与内存映射文件

页框回收

页框回收与页面置换是一体两面的问题。置换算法决定"淘汰谁",页框回收决定"什么时候淘汰"和"回收多少"。现代 Linux 内核的页框回收由 kswapd 内核线程和直接回收(Direct Reclaim)机制共同驱动,使用基于 LRU 链表(active list / inactive list)的方案,本质上是改进型 CLOCK 的链表化变体。

内存映射文件(Memory-Mapped Files, mmap)

内存映射文件是一种特殊的 I/O 机制:操作系统将一个文件的内容直接映射到进程的虚拟地址空间中。之后,进程对这片内存区域的读写操作,会被操作系统透明地转换为对文件的读写------不再需要使用 read() / write() 系统调用。

在 Linux 上,通过 mmap() 系统调用实现:

c 复制代码
// 将文件 data.bin 映射到进程的地址空间
int fd = open("data.bin", O_RDWR);
void *addr = mmap(NULL, file_size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);
// 此后 addr[100] = 0xFF 等同于直接写入文件的偏移 100 处

mmap 相比传统 read/write 有两大优势:

  1. 减少数据拷贝次数:read/write 需要"磁盘 → 内核缓冲区 → 用户缓冲区"两次拷贝;mmap 直接映射内核页缓存到用户空间,省掉一次拷贝。
  2. 操作系统负责 I/O 调度和缓存管理:脏页由内核统一写回,不需要应用程序手动管理刷盘时机。

但这也有代价:对于顺序只读的小文件,read/write 的预读(Readahead)机制通常更高效;mmap 在处理随机访问的大文件(如数据库存储引擎)时优势更明显------这也是为什么 MongoDB(WiredTiger)和 LMDB 等数据库广泛使用 mmap。

⚠️ 注意事项:mmap 的错误处理比 read/write 更微妙------写入映射区后程序可能"以为"写成功了(因为没有返回错误码),但实际 I/O 错误(如磁盘满)可能在稍后才以 SIGBUS 信号的形式暴露。这是工程中使用 mmap 需要额外处理的边界情况。


FAQ:常见问题速查

Q: CLOCK 算法为什么比真正的 LRU 更实用?

真正的 LRU 需要每次访存都更新硬件数据结构(计数器或栈),这在现代 CPU 上意味着每次内存访问都附带额外的寄存器和比较操作------性价比太低。CLOCK 只需要一个 1-bit 的访问位,由硬件在页表遍历时自动设置,软件只需在置换时读取和清零------硬件成本趋近于零。改进型 CLOCK 用两个位(A + M)实现的效果已经非常接近 LRU。

Q: Belady 异常在考试中如何快速判断和计算?

先判断算法类型:如果题目中出现 FIFO → 可能存在 Belady 异常;如果是 LRU 或 OPT → 一定没有。具体验证方法:手工建表,给定页访问串,分别计算 N 和 N+1 个页框的缺页次数。若缺页次数不降反升,则 Belady 异常成立。考试中典型的验证从 3 个页框开始,分别算 3 和 4 个页框的结果即可。

Q: mmap 有什么风险?什么时候不应该用它?

mmap 的主要风险:一是文件大小不可变时映射会失败或产生 SIGBUS(所以更新文件大小时需要 fallocate);二是错误检测被延迟(写入时不报告 I/O 错误);三是映射大文件可能导致页表占用过多的内存(因为每个映射页都需要页表项)。不适合用 mmap 的场景包括:流式顺序读(read 的预读更高效)、频繁扩展缩小的文件、网络文件系统上的文件(因为 page cache 一致性语义的差异)。

Q: 如何从系统行为判断是否进入抖动?

典型信号是磁盘持续高 I/O 但 CPU 利用率很低------说明 CPU 在干等磁盘完成页面换入换出。工具层面:Linux 上 vmstat 1 查看 si(swap in)和 so(swap out)列,如果持续高位且 us(user CPU)很低,很可能正在抖动。iostat 也可以辅助确认磁盘 I/O 是否是瓶颈。


总结

从 OPT 的理论基准到 CLOCK 的工程实践、从 Belady 异常到工作集模型、从页面置换到页框回收------这一篇走完了虚拟内存管理中最核心也是考点最密集的部分。页面置换算法的选择直接影响系统的缺页率和吞吐量;工作集模型为动态页框分配提供了理论框架;抖动防治关系到多道程序系统的稳定性。而 mmap 作为虚拟内存在 I/O 领域的延伸,至今仍然是高性能存储系统的核心机制。

至此,OS 第三章「内存管理」的五篇文章全部完成。从最基础的地址绑定开始,到连续分配、分页、分段、虚拟内存、页面置换算法,再到页框回收与内存映射文件------希望这个系列能帮助你在 408 统考中建立起一套完整的、可以自述的内存管理知识体系。

INTERNAL-LINK: OS 操作系统系列总览 → 408 操作系统全部已发布章节


📚 延伸阅读


相关推荐
qo_tn4 小时前
Windows route 命令详解 --- 多网络环境下,指定特定地址
操作系统
qo_tn4 小时前
Windows PowerShell Test-NetConnection 使用详解
操作系统
CoovallyAIHub1 天前
Coco 在病房:一个企业级 AI Agent,如何帮住院医师省下每天三小时的文书时间
操作系统·agent·产品
纵有疾風起1 天前
文件共享与保护:从硬链接到访问控制矩阵
操作系统·文件管理·408·文件共享·硬链接·软链接
噜~噜~噜~2 天前
操作系统笔记-1.1.3 操作系统的特征
笔记·操作系统
我命由我123452 天前
Java 开发 - List subList 方法
java·windows·操作系统·list·intellij-idea·idea·intellij idea
纵有疾風起5 天前
计算机网络的性能指标体系:带宽、时延、吞吐量
计算机网络·rtt·408·带宽·性能指标·吞吐量·时延
charlie1145141915 天前
Cinux —— 给物理内存建账本:bitmap 物理内存管理器
开发语言·c++·操作系统·开源项目