当你在终端敲下./a.out后,程序突然卡顿了几秒——这可能是操作系统正在后台默默执行页面置换。作为计算机专业学生或准备面试的开发者,理解这些算法不仅是为了应付考试,更是为了写出更高效的程序。本文将带你用C++完整实现OPT、FIFO、LRU和CLOCK四种经典算法,每个实现都附带可运行的代码和详细解释。
在开始编码前,我们先搭建一个统一的测试环境。使用任何支持C++11的编译器(如g++ 7.0+或Visual Studio 2017+),创建名为page_replacement.cpp的文件:
cpp复制#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
// 打印当前物理块状态
void printFrames(const vector<int>& frames) {
for (int frame : frames) {
if (frame == -1) cout << "[ ] ";
else cout << "[" << frame << "] ";
}
cout << endl;
}
这个基础框架包含必要的头文件和打印函数。我们使用vector存储物理块状态,-1表示空块。接下来定义统一的测试用例:
cpp复制int main() {
vector<int> page_sequence = {7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1};
int frame_count = 3; // 物理块数
// 这里将调用各个算法实现
// fifo(page_sequence, frame_count);
// lru(page_sequence, frame_count);
// opt(page_sequence, frame_count);
// clock(page_sequence, frame_count);
return 0;
}
先进先出(FIFO)算法就像食堂排队——最早来的人最先离开。我们需要维护一个指针来追踪最早进入的页面:
cpp复制void fifo(const vector<int>& pages, int frame_count) {
vector<int> frames(frame_count, -1);
unordered_map<int, bool> in_memory;
int pointer = 0, faults = 0;
for (int i = 0; i < pages.size(); ++i) {
cout << "访问页面: " << pages[i] << endl;
if (!in_memory[pages[i]]) {
cout << "缺页中断! ";
if (frames[pointer] != -1) {
in_memory[frames[pointer]] = false;
}
frames[pointer] = pages[i];
in_memory[pages[i]] = true;
pointer = (pointer + 1) % frame_count;
faults++;
cout << "置换后状态: ";
printFrames(frames);
} else {
cout << "页面已在内存中" << endl;
}
}
cout << "总缺页次数: " << faults << endl;
}
关键点解析:
pointer循环遍历物理块,模拟队列行为unordered_map快速判断页面是否已在内存测试输出会显示每次缺页时的置换过程和最终缺页次数。FIFO虽然简单,但存在Belady异常——增加物理块有时反而会增加缺页率。
最近最少使用(LRU)算法需要跟踪页面的访问时间。我们使用哈希表+双向链表实现O(1)复杂度的操作:
cpp复制struct Node {
int key;
Node *prev, *next;
Node(int k) : key(k), prev(nullptr), next(nullptr) {}
};
class LRUCache {
private:
unordered_map<int, Node*> cache;
Node *head, *tail;
int capacity, size;
void addToHead(Node* node) {
node->next = head->next;
node->prev = head;
head->next->prev = node;
head->next = node;
}
void removeNode(Node* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
void moveToHead(Node* node) {
removeNode(node);
addToHead(node);
}
Node* removeTail() {
Node* node = tail->prev;
removeNode(node);
return node;
}
public:
LRUCache(int cap) : capacity(cap), size(0) {
head = new Node(-1);
tail = new Node(-1);
head->next = tail;
tail->prev = head;
}
int access(int key) {
if (cache.count(key)) {
moveToHead(cache[key]);
return 0; // 命中
} else {
if (size == capacity) {
Node* removed = removeTail();
cache.erase(removed->key);
delete removed;
size--;
}
Node* node = new Node(key);
cache[key] = node;
addToHead(node);
size++;
return 1; // 缺页
}
}
};
void lru(const vector<int>& pages, int frame_count) {
LRUCache cache(frame_count);
int faults = 0;
for (int i = 0; i < pages.size(); ++i) {
cout << "访问页面: " << pages[i] << endl;
faults += cache.access(pages[i]);
}
cout << "总缺页次数: " << faults << endl;
}
这种实现虽然复杂,但展示了工业级LRU的常见实现方式。每次访问页面时,命中则移动到链表头部,缺页则淘汰尾部页面。
最佳置换(OPT)算法需要预知未来,虽然实际无法实现,但对理解算法极限很有帮助:
cpp复制void opt(const vector<int>& pages, int frame_count) {
vector<int> frames(frame_count, -1);
unordered_map<int, bool> in_memory;
int faults = 0;
for (int i = 0; i < pages.size(); ++i) {
cout << "访问页面: " << pages[i] << endl;
if (!in_memory[pages[i]]) {
cout << "缺页中断! ";
faults++;
if (find(frames.begin(), frames.end(), -1) != frames.end()) {
// 还有空帧
auto it = find(frames.begin(), frames.end(), -1);
*it = pages[i];
in_memory[pages[i]] = true;
} else {
// 需要置换
int farthest = -1, replace_idx = 0;
for (int j = 0; j < frames.size(); ++j) {
int k = i + 1;
while (k < pages.size() && pages[k] != frames[j]) k++;
if (k > farthest) {
farthest = k;
replace_idx = j;
}
}
in_memory[frames[replace_idx]] = false;
frames[replace_idx] = pages[i];
in_memory[pages[i]] = true;
}
cout << "置换后状态: ";
printFrames(frames);
} else {
cout << "页面已在内存中" << endl;
}
}
cout << "总缺页次数: " << faults << endl;
}
OPT算法通过向后扫描页面序列,选择最长时间不会被访问的页面淘汰。虽然不实用,但它的缺页率是理论下限。
CLOCK(又称二次机会)算法通过环形队列和访问位实现近似LRU的效果:
cpp复制void clock(const vector<int>& pages, int frame_count) {
vector<int> frames(frame_count, -1);
vector<bool> ref_bits(frame_count, false);
unordered_map<int, int> page_to_index;
int pointer = 0, faults = 0;
for (int i = 0; i < pages.size(); ++i) {
cout << "访问页面: " << pages[i] << endl;
if (page_to_index.count(pages[i])) {
// 页面已在内存中
int idx = page_to_index[pages[i]];
ref_bits[idx] = true;
cout << "设置引用位" << endl;
} else {
// 缺页处理
cout << "缺页中断! ";
faults++;
while (true) {
if (frames[pointer] == -1) {
// 找到空帧
frames[pointer] = pages[i];
ref_bits[pointer] = true;
page_to_index[pages[i]] = pointer;
pointer = (pointer + 1) % frame_count;
break;
} else if (!ref_bits[pointer]) {
// 找到淘汰页
page_to_index.erase(frames[pointer]);
frames[pointer] = pages[i];
ref_bits[pointer] = true;
page_to_index[pages[i]] = pointer;
pointer = (pointer + 1) % frame_count;
break;
} else {
// 给第二次机会
ref_bits[pointer] = false;
pointer = (pointer + 1) % frame_count;
}
}
cout << "置换后状态: ";
printFrames(frames);
}
}
cout << "总缺页次数: " << faults << endl;
}
CLOCK算法的指针环形扫描,遇到引用位为1的页面会将其置0并跳过,为0的页面则被置换。这种实现比纯LRU更高效,是许多操作系统的实际选择。
四种算法在实际应用中各有优劣:
| 算法 | 时间复杂度 | 需要硬件支持 | 备注 |
|---|---|---|---|
| FIFO | O(1) | 不需要 | 可能产生Belady异常 |
| LRU | O(1) | 需要 | 最接近OPT但实现复杂 |
| OPT | O(n^2) | 不可能 | 理论参考值 |
| CLOCK | O(1) | 需要 | 工程折中方案,广泛使用 |
在真实系统设计中,通常会考虑以下因素:
cpp复制// 测试所有算法
void testAll(const vector<int>& pages, int frame_count) {
cout << "=== FIFO 算法 ===" << endl;
fifo(pages, frame_count);
cout << "\n=== LRU 算法 ===" << endl;
lru(pages, frame_count);
cout << "\n=== OPT 算法 ===" << endl;
opt(pages, frame_count);
cout << "\n=== CLOCK 算法 ===" << endl;
clock(pages, frame_count);
}
运行这个测试函数,你会直观看到不同算法在相同访问序列下的表现差异。建议尝试修改页面序列和物理块数量,观察算法行为的变化规律。