页面置换算法引入
页面置换:在系统运行过程中,若程序所要访问的页面不在内存而需要把他们调入内存,但内存已经没有空闲空间时,为了保证该进程能正常运行,系统必须从内存中调出一页程序或数据送到磁盘的交换区中,这个过程称为页面置换。
页面置换算法:置换算法决定将哪个页面调出,需根据一定的算法来确定,通常,把选择换出页面的算法成为页面置换算法。
FIFO算法
FIFO(First in First out),先进先出。
其实在操作系统的设计理念中很多地方都利用到了先进先出的思想,比如作业调度(先来先服务),为什么这个原则在很多地方都会用到呢?因为这个原则简单、且符合人们的惯性思维,具备公平性,并且实现起来简单,直接使用数据结构中的队列即可实现。
-
在FIFO Cache设计中,核心原则就是:如果一个数据最先进入缓存中,则应该最早淘汰掉。也就是说,当缓存满的时候,应当把最先进入缓存的数据给淘汰掉。
-
FIFO算法还会产生当所分配的物理块数增大而页故障数不减反增的异常现象,这是由 Belady于1969年发现,故称为
Belady异常
,如图所示。 -
只有FIFO算法可能出现Belady 异常,而LRU和OPT算法永远不会出现Belady异常。
-
在FIFO Cache中应该支持以下操作
get(key):如果Cache中存在该key,则返回对应的value值,否则,返回-1;
set(key,value):如果Cache中存在该key,则重置value值;如果不存在该key,则将该key插入到到Cache中,若Cache已满,则淘汰最早进入Cache的数据。
那么利用什么数据结构来实现呢?
下面提供一种实现思路:
- 利用一个双向链表保存数据,当来了新的数据之后便添加到链表末尾,如果Cache存满数据,则把链表头部数据删除,然后把新的数据添加到链表末尾。
- 在访问数据的时候,如果在Cache中存在该数据的话,则返回对应的value值;否则返回-1。
- 如果想提高访问效率,可以利用hashmap来保存每个key在链表中对应的位置。
LFU算法
LFU(Least Frequently Used)最近最少使用算法。
-
设计思路:基于
如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小
-
注意LFU和LRU算法的不同之处,LRU的淘汰规则是基于访问时间,而LFU是基于访问次数的。
-
那么LFU Cache应该支持的操作为:
get(key):如果Cache中存在该key,则返回对应的value值,否则,返回-1;
set(key,value):如果Cache中存在该key,则重置value值;如果不存在该key,则将该key插入到到Cache中,若Cache已满,则淘汰最少访问的数据。
LFU算法最简单的一种设计思路:
- 利用一个数组存储 数据项
- 用hashmap存储每个数据项在数组中对应的位置
- 然后为每个数据项设计一个访问频次,当数据项被命中时,访问频次自增,在淘汰的时候淘汰访问频次最少的数据。
- 这样一来的话,在插入数据和访问数据的时候都能达到O(1)的时间复杂度,在淘汰数据的时候,通过选择算法得到应该淘汰的数据项在数组中的索引,并将该索引位置的内容替换为新来的数据内容即可,这样的话,淘汰数据的操作时间复杂度为O(n)。
另外还有一种实现思路就是利用 小顶堆+hashmap,小顶堆插入、删除操作都能达到O(logn)时间复杂度,因此效率相比第一种实现方法更加高效。
LRU算法
LRU(Least Recently Used)即最近最久未使用算法
- LRU算法的设计原则是:
如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小
。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。 - 那么LRU Cache应该支持的操作为
get(key): 如果key在cache中,则返回对应的value值,否则返回-1
set(key,value):如果key不在cache中,则将该(key,value)插入cache中(注意,如果cache已满,则必须把最近最久未使用的元素从cache中删除);如果key在cache中,则重置value的值。
- LRU算法最简单的一种设计思路
- 用一个数组来存储数据,给每一个数据项标记一个访问时间戳
- 每次插入新数据项的时候,先把数组中存在的数据项的时间戳自增,并将新数据项的时间戳置为0并插入到数组中。
- 每次访问数组中的数据项的时候,将被访问的数据项的时间戳置为0。
- 当数组空间已满时,将时间戳最大的数据项淘汰。
这种实现思路很简单,但是需要不停地维护数据项的访问时间戳,另外,在插入数据、删除数据以及访问数据时,时间复杂度都是O(n)。
- 利用链表和hashmap
- 当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部。
- 若缓存满了,则把链表最后一个节点删除即可。
- 在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。
- 这样一来在链表尾部的节点就是最近最久未访问的数据项。
代码实现:
#include <iostream>
#include <map>
#include <algorithm>
#include <list>
using namespace std;
struct Node
{
int key;
int value;
};
class LRUCache{
private:
int maxSize ;
list<Node> cacheList;
map<int, list<Node>::iterator > mp;
public:
LRUCache(int capacity) {
maxSize = capacity;
}
int get(int key) {
map<int, list<Node>::iterator >::iterator it = mp.find(key);
if(it==mp.end()) //没有命中
{
return -1;
}
else //在cache中命中了
{
list<Node>::iterator listIt = mp[key];
Node newNode;
newNode.key = key;
newNode.value = listIt->value;
cacheList.erase(listIt); //先删除命中的节点
cacheList.push_front(newNode); //将命中的节点放到链表头部
mp[key] = cacheList.begin();
}
return cacheList.begin()->value;
}
void set(int key, int value) {
map<int, list<Node>::iterator >::iterator it = mp.find(key);
if(it==mp.end()) //没有命中
{
if(cacheList.size()==maxSize) //cache满了
{
mp.erase(cacheList.back().key);
cacheList.pop_back();
}
Node newNode;
newNode.key = key;
newNode.value = value;
cacheList.push_front(newNode);
mp[key] = cacheList.begin();
}
else //命中
{
list<Node>::iterator listIt = mp[key];
cacheList.erase(listIt); //先删除命中的节点
Node newNode;
newNode.key = key;
newNode.value = value;
cacheList.push_front(newNode); //将命中的节点放到链表头部
mp[key] = cacheList.begin();
}
}
};
最佳页面置换算法(OPT)
最优置换(Optimal Replacement)是在理论上提出的一种算法。
- 其实质是:当调入新的一页而必须预先置换某个老页时,
所选择的老页应是将来不再被使用,或者是在最远的将来才被访问。
- 采用这种页面置换算法,保证有最少的缺页率。
- 但是最优页面置换算法的实现是困难的,因为它需要人们预先就知道一个进程整个运行过程中页面走向的全部情况。
时钟(CLOCK)置换算法
- LRU算法的性能接近于OPT,但是实现起来比较困难,且开销大
- FIFO算法实现简单,但性能差。
- 所以操作系统的设计者尝试了很多算法,
试图用比较小的开销接近LRU的性能,这类算法都是CLOCK算法的变体。
- 简单的CLOCK算法是给每一帧关联一个附加位,称为使用位。
- 当某一页首次装入主存时,该帧的使用位设置为1
- 当该页随后再被访问到时,它的使用位也被置为1。
- 对于
页替换算法
,用于替换的候选帧集合看做一个循环缓冲区,并且有一个指针与之相关联。 - 当某一页被替换时,该指针被设置成指向缓冲区中的下一帧。
- 当需要替换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一帧。
- 每当遇到一个使用位为1的帧时,操作系统就将该位重新置为0
- 如果在这个过程开始时,缓冲区中所有帧的使用位均为0,则选择遇到的第一个帧替换
- 如果所有帧的使用位均为1,则指针在缓冲区中完整地循环一周,把所有使用位都置为0,并且停留在最初的位置上,替换该帧中的页。
- 由于该算法循环地检查各页面的情况,故称为CLOCK算法,又称为最近未用(Not Recently Used, NRU)算法。
改进型的CLOCK置换算法
CLOCK算法的性能比较接近LRU,而通过增加使用的位数目,可以使得CLOCK算法更加高效。在使用位的基础上再增加一个修改位,则得到改进型的CLOCK置换算法。这样,每一帧都处于以下四种情况之一:
最近未被访问,也未被修改(u=0, m=0)。
最近被访问,但未被修改(u=1, m=0)。
最近未被访问,但被修改(u=0, m=1)。
最近被访问,被修改(u=1, m=1)。
算法执行如下操作步骤:
- 从指针的当前位置开始,扫描帧缓冲区。在这次扫描过程中,对使用位不做任何修改。选择遇到的第一个帧(u=0, m=0)用于替换。
- 如果第1)步失败,则重新扫描,查找(u=0, m=1)的帧。选择遇到的第一个这样的帧用于替换。在这个扫描过程中,对每个跳过的帧,把它的使用位设置成0。
- 如果第2)步失败,指针将回到它的最初位置,并且集合中所有帧的使用位均为0。重复第1步,并且如果有必要,重复第2步。这样将可以找到供替换的帧。
改进型的CLOCK算法优于简单CLOCK算法之处在于替换时首选没有变化的页。由于修改过的页在被替换之前必须写回,因而这样做会节省时间。