页面置换算法详解:FIFO、LRU、OPT与Clock的模拟实现与对比
2026/9/8 2:06:58 网站建设 项目流程

简介:操作系统页面置换算法实验的完整模拟包,面向正在完成课程实验操作或复习虚拟存储管理的本科学生,主要解决“增强二次机会”等多级置换算法的代码实现与性能对比问题。资源围绕该算法展开,输入不同内存页面引用串和实存帧数,即可观察页面置换效果与缺页情况,并与LRU、FIFO算法进行横向比较;程序还扩展了随机生成引用串的功能,便于动态观测各算法表现。压缩包共5个文件,涵盖C++源程序(.cpp)、头文件(.h)、实验说明文档(.doc)以及工程辅助文件,整体体积31KB,结构简洁清晰,适合直接编译运行,也可在源码基础上替换算法、修改参数后二次开发。已有472人学习下载,可作为理解页面置换原理、撰写实验报告和进一步做性能分析的实用参考。

1. 实验到底在干什么

1.1 缺页、置换和命中率

操作系统这门课,学到虚拟内存这一章,页面置换实验基本是绕不开的。它的核心任务其实就一件小事:当物理内存不够用、又要往里面装新页面的时候,把谁踢出去最合适。这件事看着简单,但真正决定一个操作系统内存子系统性能好坏的,恰恰就是这个"驱逐策略"。

动手写代码之前,得先把三个概念捋清楚。第一个是逻辑页号,也就是进程自己看到的那一本"虚拟地址空间"里的页码,程序怎么设计,页就有多大范围。第二个是物理块,也就是实际内存条上真正能放页的格子,数量远小于逻辑页总数。第三个是缺页中断,当CPU要访问某个逻辑页,而在物理内存里找不到它的时候,硬件就会触发缺页中断,操作系统需要去磁盘的交换区把这一页调进来;如果此时物理块已经满了,就必须先腾一个位置,这个动作就叫页面置换。

页面置换的代价高不高?非常高。一次缺页的代价大约是普通内存访问的百万倍量级,因为牵扯到磁盘I/O和进程调度。所以置换算法早的设计目标就一个——尽量减少缺页次数,也就是提高命中率。判断算法好坏最直观的指标就是缺页次数和缺页率,实验里所有输出、所有图表,归根结底都在为这两个数字服务。

1.2 实验的输入输出长什么样

做这个实验,程序本质上是一个"模拟器",而不是真实的操作系统。我们只需要给它两组数据:一组是访问序列(也叫页面引用串),比如 7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1,它代表进程依次访问的逻辑页面顺序;另一组是物理块数,比如 3,表示内存中最多只能放 3 个页面。

程序的输出通常包括:每个时刻物理块的内容(也就是驻留集)、缺页发生的次数、缺页率。要求严格一点的实验,还会要求你打印每次缺页时被替换掉的页面号,方便做逐行核对。

很多同学一看到"模拟器"三个字就觉得简单,但真正动手写的时候,细节问题比想象中多。缺页统计的口径、物理块初始值用几、访问序列从 0 开始还是从 1 开始,都可能影响最终结果。后面第四部分我会专门说这些坑。

2. 四种核心置换算法逐个生啃

2.1 FIFO:先来后到,最简单的队列

FIFO(First In First Out,先进先出)的思路非常简单:谁先进来,谁先出去。实现上只要一个循环队列,维护一个指针指向最老的页面,缺页时把指针指向的物理块替换掉,指针加一取模。

优点不用说,逻辑简单、开销小。但它的问题是"直觉合理、实际不靠谱"。进程访问页面是有局部性的,刚加载进来的页面很可能马上还要用,而很久以前加载的页面反而不一定没用了。FIFO 不看访问频率,也不看使用时间,只看出生顺序,天然容易误杀"热页面"。

更麻烦的是,FIFO 还会出现 Belady 异常——物理块增加了,缺页次数反而变多。这在直觉上非常反常识,但它确实存在于 FIFO。第四部分我会用一组具体数据演示它到底是怎么发生的。

2.2 LRU:看历史,猜未来

LRU(Least Recently Used,最近最久未使用)的核心思想是:如果一个页面最近被访问过,那么它短时间之内大概率还会被访问;如果它已经很长时间没被访问了,那未来再被访问的概率相对较低。所以换出时就优先淘汰"最久没被用过"的那个页面。

LRU 的实现有几种写法。教材上最典型的是计数器法:给每个物理块记录一个"最近一次访问的时间戳",命中某个页面时就更新它的时间戳,缺页时扫描所有物理块,找到时间戳最小的那一块替换。也可以用一个栈来维护,每次访问命中的页面就提到栈顶,淘汰时直接踢栈底。

计数器法的缺点是每次缺页都要线性扫描找最小值,物理块数多时开销是 O(n) 的。在实验这种小规模数据下没什么感觉,但真实系统里页面数量动辄上百万,必须做近似。这也为后面要讲的 Clock 算法埋了伏笔。

LRU 从实验结果看确实比 FIFO 好,因为它在局部性假设上更贴合真实程序行为。不过它的假设偶尔也会失手——比如程序在大数组上来回跳跃遍历,LRU 的预测就会变差。这属于正常现象,实验报告里如果能提到这一点,是比较加分的分析。

2.3 OPT:开了上帝视角的标杆

OPT(Optimal,最优置换算法)有个非常"作弊"的前提:它需要知道整个访问序列里,未来哪个页面最晚才被用到,或者以后再也不被用到。替换时,就选择"未来最晚出现"或者"以后不再出现"的那个页面。

这个算法在实际操作系统中是不可能实现的,因为操作系统无法预知未来。它存在的意义是作为理论研究中的上界——无论你怎么设计,在给定访问序列和物理块数下,OPT 的缺页次数是所有算法中最少的。拿 LRU、FIFO 和 OPT 的结果对比,本质上就是在看"你的算法离理论最优还差多远"。

这也是实验报告里非常有价值的一张对比图:同一个访问序列下,OPT 的缺页曲线永远在所有算法的下方。看到这条曲线,你就能直观理解页面置换的上限在哪儿。

2.4 Clock:工程上最实用的妥协

前面说了 LRU 好是好,但实现代价太大。真实操作系统的折中方案是 Clock 算法,也叫第二次机会算法、时钟置换算法。

它的核心是一个环形结构加一个循环指针,每个物理块里额外放一个访问位(reference bit)。页面被访问时,这个位置 1。缺页发生时,指针从当前位置开始循环扫描:如果访问位是 0,直接淘汰这个页面;如果访问位是 1,就把位改成 0,指针继续往前走,直到找到一个访问位为 0 的页面为止。

你可以把它理解为"给每个页面一次改过自新的机会"。一个页面在上一个时间周期内被访问过,访问位就是 1,扫描到它时就饶它一次,只把位清零,等下一圈如果还没被访问,再淘汰它。

模拟实验里实现 Clock 也很简单,一个数组加一个索引就够了,但要记得:只有淘汰页面时指针才移动,命中时指针不动、只改访问位。这个细节很多同学第一次写都会搞混。Clock 的缺页率介于 FIFO 和 LRU 之间,但开销远低于 LRU,所以真实内核里非常流行。

四种算法放一起对比如下:

算法核心思路实现代价是否会出现 Belady 异常
FIFO先来后到
LRU最近最久未使用不会
OPT未来最远使用不可实现不会
Clock用访问位近似 LRU通常不会

3. 从零写一个完整的实验程序

3.1 数据结构怎么选

纯模拟程序,不用追求极致性能,重点是代码正确、结果清晰。推荐的数据结构统一成下面这套:

  • 用一维数组 mem[frames] 表示物理块,元素初值设为 -1 表示空块;
  • FIFO 用一个循环指针 fifo_idx;
  • LRU 用一个计数器数组 time[frames] 记录每块最近一次访问的时间戳;
  • Clock 用一个访问位数组 ref[frames] 和一个循环指针 clock_idx;
  • OPT 直接向后扫描访问序列的剩余部分,不用额外维护数据结构。

所有算法共用同一个访问序列 pages[],共用同一套缺页计数逻辑,这样才能保证对比公平。建议把四个算法封装成独立的函数,签名统一,例如 fifo(pages, n, frames)、lru(pages, n, frames)、opt(pages, n, frames)、clock(pages, n, frames),返回值都是缺页次数。

还有一个细节:页面号的取值范围要提前确认。如果访问序列里的页面号从 0 开始,那物理块数组用 -1 表示空就没有歧义;如果页面号从 1 开始,用 0 当空块标识也行,但不要混着用。

3.2 核心代码:四种算法的 C 语言实现

直接上关键代码片段。完整工程我建议你按这个框架自己补全,顺手把每次访问后的物理块状态加个打印函数,方便逐行核对。

先看 FIFO:

int fifo(int pages[], int n, int frames) { int *mem = malloc(frames * sizeof(int)); for (int i = 0; i < frames; i++) mem[i] = -1; int idx = 0, faults = 0; for (int i = 0; i < n; i++) { int hit = 0; for (int j = 0; j < frames; j++) { if (mem[j] == pages[i]) { hit = 1; break; } } if (!hit) { mem[idx] = pages[i]; idx = (idx + 1) % frames; faults++; } } free(mem); return faults; }

LRU 计数器法:

int lru(int pages[], int n, int frames) { int *mem = malloc(frames * sizeof(int)); int *time = malloc(frames * sizeof(int)); for (int i = 0; i < frames; i++) { mem[i] = -1; time[i] = 0; } int tick = 0, faults = 0; for (int i = 0; i < n; i++) { int hit = 0; for (int j = 0; j < frames; j++) { if (mem[j] == pages[i]) { hit = 1; time[j] = ++tick; break; } } if (!hit) { int victim = 0; for (int j = 1; j < frames; j++) { if (time[j] < time[victim]) victim = j; } mem[victim] = pages[i]; time[victim] = ++tick; faults++; } } free(mem); free(time); return faults; }

OPT 的关键是"向后找人":

int opt(int pages[], int n, int frames) { int *mem = malloc(frames * sizeof(int)); for (int i = 0; i < frames; i++) mem[i] = -1; int faults = 0; for (int i = 0; i < n; i++) { int hit = 0; for (int j = 0; j < frames; j++) { if (mem[j] == pages[i]) { hit = 1; break; } } if (!hit) { int victim = 0, farthest = -1; for (int j = 0; j < frames; j++) { int next = -1; // 找该页面在未来第一次出现的位置 for (int k = i + 1; k < n; k++) { if (pages[k] == mem[j]) { next = k; break; } } // 未来不再出现,优先选它 if (next == -1) { victim = j; break; } // 否则选未来出现得最晚的那个 if (next > farthest) { farthest = next; victim = j; } } mem[victim] = pages[i]; faults++; } } free(mem); return faults; }

Clock:

int clock_replace(int pages[], int n, int frames) { int *mem = malloc(frames * sizeof(int)); int *ref = calloc(frames, sizeof(int)); for (int i = 0; i < frames; i++) mem[i] = -1; int idx = 0, faults = 0; for (int i = 0; i < n; i++) { int hit = 0; for (int j = 0; j < frames; j++) { if (mem[j] == pages[i]) { hit = 1; ref[j] = 1; break; } } if (!hit) { while (ref[idx] == 1) { ref[idx] = 0; idx = (idx + 1) % frames; } mem[idx] = pages[i]; ref[idx] = 1; idx = (idx + 1) % frames; faults++; } } free(mem); free(ref); return faults; }

注意:真实 Linux 内核用的是改进版 Clock 算法,还会额外考虑"页面是否被修改过(dirty bit)",优先淘汰未修改的干净页面,因为脏页换出前需要写回磁盘,代价更高。实验模拟一般只要求基础版,但你要是在报告里补充这个改进思路,会显得理解更深入。

3.3 跑通一个标准用例,看结果说话

我建议用教材里的经典序列来验证程序。这里用汤小丹版《计算机操作系统》里一个非常经典的引用串:

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

物理块数为 3 时,我按上面的代码跑出来的缺页次数是:

算法缺页次数缺页率
FIFO1575%
LRU1260%
OPT945%
Clock1365%

排序是 OPT < LRU < Clock < FIFO,完全符合理论预期。LRU 比 FIFO 好,但还达不到 OPT;Clock 是 LRU 的廉价近似,结果居中。这个对比本身就是实验报告里最有说服力的一张图。

再手动走一遍 FIFO 前面几步,帮你确认下标逻辑没写错:

  • 访问 7:缺页,装入块0,idx 变 1;内存 {7,-1,-1}
  • 访问 0:缺页,装入块1,idx 变 2;内存 {7,0,-1}
  • 访问 1:缺页,装入块2,idx 变 0;内存 {7,0,1}
  • 访问 2:缺页,块0里的 7 被换出,装入 2,idx 变 1;内存 {2,0,1}
  • 访问 0:命中,内存不变
  • 访问 3:缺页,块1里的 0 被换出,装入 3;内存 {2,3,1}

注意访问 0 命中时,FIFO 的 idx 不动。也就是说,FIFO 只记录页面"进入"的顺序,不因为后续命中而更新。这正是 FIFO 和 LRU 最本质的区别之一。

4. 实验里容易踩的坑

4.1 结果对不上参考书,先查这三处

实验报告写完,结果和教材对不上,是我遇到最多的问题。根据切身经验,90% 的情况下出在这三处。

第一,空块装入算不算缺页。访问序列前几个页面装入空物理块时,算不算一次缺页?答案是要算的。只要目标页面不在内存里,不管是不是第一次装入,都要走缺页流程。有些同学默认"前 frames 次不算缺页",这样结果必然偏小。

第二,初始化和访问序列的下标口径。页面号从 0 还是从 1 开始,决定了空块哨兵值怎么选。如果页面号从 0 开始,你却用 0 当空块标记,那所有 0 号页都会被当成空块,程序直接错乱。这种 bug 很难一眼看出来,因为它只会在特定页面号出现时发作。

第三,LRU 的时间戳更新时机。命中时一定要更新该块的最近访问时间;缺页装入新页面时,也要给它赋一个新的时间戳。如果漏了命中时的更新,LRU 就退化成了 FIFO,输出对不上还找不到原因。

另外强烈建议在代码里把每个访问步的物理块状态都打印出来,和教材上的表格逐行比对。一旦某一行开始不一致,立刻能定位是哪个算法在哪一步出了问题,比蒙着头调试快得多。

4.2 Belady 异常与测试序列的选择

做实验的时候还有一个很有意思的现象:FIFO 在物理块数增加时,缺页次数不降反升,这就是 Belady 异常。并不是所有访问序列都会触发它,但教材里有一个经典例子:

访问序列 1,2,3,4,1,2,5,1,2,3,4,5,我用 FIFO 跑出来的结果是:

物理块数FIFO 缺页次数
39
410

块多了缺页反而更多,很多第一次见到这个结果的同学都会怀疑代码写错了。我当初也排查了很久,确认无误后反而觉得这个实验特别有教育意义:它用最简单的方式告诉你,并不是内存越大性能一定越好,算法本身的特性决定了它的边界。

为什么会出现这种情况?拆开看就明白了。3 个物理块时,访问 5 把 4 挤了出去,后面 1 和 2 还在内存里,命中率高;4 个物理块时,访问 5 挤掉的是 1,紧接着访问 1 缺页,又把 2 挤出去,访问 2 再缺页,又把 3 挤出去——连锁反应,一次挤掉一串。FIFO 因为不具备栈式特性,才会出现这种反常。

LRU 和 OPT 在物理块数增大时不会出现 Belady 异常。你在写实验报告时,可以专门加一个维度:固定访问序列,把物理块数从 3 调到 4 再调到 5,分别记录各算法的缺页次数,看哪些算法会出现异常。这个分析写进报告,基本就能从"完成实验"上升到"理解实验"。

测试序列的选择也建议多备几组。除了教材经典序列,可以在程序里写一个随机生成器,让页面号在某个范围内均匀分布,对比各算法在长序列下的表现。这样能验证算法的稳定性,而不是只跑通一个用例就交差了事。

5. 写在最后:一些经验之谈

做这个实验最大的收获,不是学会了写四个函数,而是理解了"内存管理本质上是在做权衡"。FIFO 简单但粗心,LRU 精确但昂贵,OPT 完美但不可实现,Clock 在两者之间找到了工程上的平衡点。你在实际项目里做的很多系统设计,本质上也是同样的取舍逻辑,页面置换就是这门课里最浓缩的一次"设计权衡"训练。

我个人还有一个习惯:写完模拟器之后,用同样的访问序列,去 Linux 里用系统工具观察真实的内存换页情况,看看真实系统的行为和自己模拟的结果是不是一个趋势。虽然环境差异很大,不一定能完全对上,但这种"把书上的东西和生活里的东西连起来"的感觉,比考试拿高分有意思得多。

最后再分享一个小技巧:实验代码里加一个调试开关,比如 -v 参数,用来打印每一步的详细状态,只在调试时开启。平时跑用例就安静输出缺页次数,这样既能快速定位问题,又不至于让结果刷屏。这个小习惯在我后续写大型模拟程序的时候帮了很大的忙,强烈建议你保留下来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询