页面置换算法
Page replacement happens when a requested page is not in memory (page fault) and a free page cannot be used to satisfy the allocation, either because there are none, or because the number of free pages is lower than some threshold. --Wikepedia
页面置换算法用于当 被请求的页面不在内存 或 空闲空间不足以满足页面载入需求 时,选择内存中原有的页面与被请求页面进行交换。
缺页率
缺页率 = 失败次数 / 加载页面的总次数
失败/成功次数 : 当被请求页面已经在内存中时,该次加载成功,否则失败并执行算法交换页面
几种页面置换算法
- 页替换算法
- 理论最优(OPT)[略]
- 先进先出(First-in, first-out)
- 最近最少使用(Least recently used)
- 两次机会(Second-chance)
- 最近未使用(Not recently userd)
- 其他
本文的重点内容在于FIFO与LRU算法的原理与实现
先进先出(First-in, first-out)
先进先出算法作为一种十分简单的页面置换算法实现,和它的名字一样,其实现原理十分简单容易理解:
按顺序替换出进入内存最久的页面
在先进先出算法中,我们不需要费劲心思的考虑如何选择换出页面可以获得最高的性价比,我们只遵循一个原则,“先进,先出” ,即当需要换出页面时,永远换出在内存中呆了最长时间的页面,固执,但是简单
实现
先进先出算法有两种较为容易理解的实现,我们依次来看一下:
1.循环队列
在该算法的经典实现中,我们使用 顺序的(数组的)循环队列作为内存的模拟
数据结构定义如下,为了清晰,这里隐藏了内部的具体实现:
/*c++*/
#include<vector>
class Queue{
public:
Queue(const int capacity = 4):capacity(capacity),cur_front(0),cur_rear(0){
/*初始化队列大小为capacity,将cur_front与cur_rear的值初始化为0*/
for(int i = 0; i != this->capacity; ++i)
myqueue.push_back(0);
}
bool push(const int e){
/*添加元素e至队列尾部*/
}
bool pop(void){
/*将队列头部位置的元素弹出*/
}
bool existed(const int e){
/*如果队列中存在元素e,返回true,否则返回false*/
}
int front(void) const{
//返回队列的首元素
return myqueue[cur_front];
}
private:
int capacity;//队列大小
vector<int> myqueue;//数组
int cur_front;//游标,指向队列的头部位置
int cur_rear;//游标,指向队列的尾部位置
};
将待执行的页面看作一个顺序数组pages,则对于其中的待执行页面page,先进先出算法的实现步骤如下:
- 在内存中寻找page是否存在
- 若找到页面,则页面曾经被加载过,无需重复加载,返回步骤1处理下一个待执行页面
- 若未找到,则表示该页面未被加载,试着将页面加载入内存
- 若内存未满,加载成功后返回步骤1处理下一个页面
- 若加载失败,说明内存已满,此时将内存头部的页面弹出,再次加载页面至尾部即可
当内存的capacity=3
待执行页面={1, 2, 3, 4, 3, 1, 4, 2, 3, 1, 4}时,算法的执行过程如下:
The pages waiting to be loaded into memory: 1 -> 2 -> 3 -> 4 -> 3 -> 1 -> 4 -> 2 -> 3 -> 1 -> 4 ->end
page 1 replace in memory.
page 2 replace in memory.
page 3 replace in memory.
page 4 replace in memory, page 1 was swapped out.
page 3 is already in memory.
page 1 replace in memory, page 2 was swapped out.
page 4 is already in memory.
page 2 replace in memory, page 3 was swapped out.
page 3 replace in memory, page 4 was swapped out.
page 1 is already in memory.
page 4 replace in memory, page 1 was swapped out.
缺页率为:0.727273
完整代码在 目录-源码
2.数组
和使用循环队列的实现方法不同,使用一些巧妙地技巧可以十分简单的使用数组完成FIFO算法的实现
/*C*/
#define CAPACITY 3//内存的容量
int memory[CAPACITY];//数组,模拟内存
int cur;//游标,标记下一个处理(加载页面、页面换出)的内存位置
/* 约定:所有的page都不为-1 */
在这个实现中,很重要的一点在于约定:所有的page页号都不为-1,利用这个约定,我们仅使用1个游标就可以判断当前内存位置的状态为空还是已经存在一个之前加载的页面
实现步骤如下:
- 初始化cur = 0, memory={-1, -1, -1}
- 搜索page是否存在于memory中
- 若存在,则页面已被加载过,返回步骤1处理下一页面
- 若不存在,检查当前memory[cur]的值
- 若memory[cur] == -1, 说明当前内存未满,令memory[cur] = page 将页面加载进内存
- 若memory[cur] != -1,说明当前内存位置已有一个页面,依旧 memory[cur] = page,替换原有页面
- ++cur %= CAPACITY, 维护cur的值
**Tip:**要理解这个实现方法,重要的是牢记cur指向的位置始终是下一个待处理的内存位置,明白算法执行过程中cur值的变化,如何维护能达到我们所需要的效果
最近最少使用(Least recently used)
最近最少使用算法的工作原理有些令人费解:
在一段时间内跟踪页面使用情况,换出页面时选择使用最少的页面
事实上仅凭其工作原理我们很难明白为什么这样的算法能够很好的运转,因此要理解LRU,关键所在就是要明白其核心思想
用过去预测未来
这一预测算法思想在很多地方都被我们所不自觉地利用(等一辆没有时刻表的公交车时,我们总猜测下一趟公交抵达的时间和我们已经等待的时间差不多),而在LRU算法中,我们就利用了这样的思想,最近最少使用算法认为如果我们在过去的一段时间用到了该页面,那么在不远的未来,这个页面同样有可能被用到
实现
在模拟实现LRU的数据结构中,我们同样使用数组表示内存,而同FIFO不同,我们还需要一个与memory同样大小的tracker数组标记内存中页面的访问记录
#include <vector>
vector<int> memory;//数组,模拟内存
vector<unsigned int> tracker;//数组,标记内存中对应页面的访问记录
int capacity;//内存的容量
const unsigned int mask;/*掩码,对于4字节大小的unsigned int来说,
掩码的值将被初始化为 (unsigned int)INT_MIN,
十六进制表示为 0x80 00 00 00,
即二进制的 1000 0000 0000 0000 0000 0000 0000 0000*/
这里有一点要解释的是mask的值和tracker的工作原理,这部分略有一些复杂,如果不感兴趣