一、功能与目标
功能:缺页中断后,需要调入新的页面而内存已满时,选择内存当中哪个物理页面被置换
目标:尽可能减少页面的换出换入次数
页面锁定:用于描述必须常驻内存的操作系统的关键部分或时间关键的应用程序。实现方法:在页表中添加锁定标志位,确保不被换出
二、局部页面置换算法
1. 最优页面置换算法
基本思路:当一个中断产生,对保存于内存中的每一个逻辑页面,计算距离它下一次访问之前,还需要等待多长时间,从中选择等待时间最长的页置换
适用场景:理想状态,因为需要预先知晓未来的页面访问情况。
2. 先进先出算法
基本思路:选择在内存中驻留时间最长的页面并淘汰。
实现方式:系统中维护链表,记录内存中逻辑页面。链表首为驻留时间最长的页面,尾部为时间最短页面。当发生缺页中断,则将链表头页面淘汰,新页面入尾部。
缺点:性能较差,调出的页面可能是经常要访问的,并且可能出现Belady现象(物理页分配越多,缺页中断次数反而更多)。
代码实现:
LinkList.java (模拟链表)
package opLearn;
public class LinkList<T> {
private class Node<T> {
T data;
Node<T> next;
}
private final Node<T> head;
private Node<T> tail;
private int size;
public LinkList() {
head = new Node<T>();
head.data = null;
head.next = null;
tail = head;
size = 0;
}
public void add(T data) {
Node<T> node = new Node<>();
node.data = data;
node.next = null;
tail.next = node;
tail = node;
size++;
}
public int find(T data) {
int loc = 0;
Node<T> point = head.next;
while (point != null) {
if (point.data == data) {
return loc;
}
point = point.next;
loc++;
}
return -1;
}
public T remove(int loc) {
int linkLoc = 0;
Node<T> point = head.next;
Node<T> prePoint = head;
while (point != null) {
if (linkLoc == loc) {
prePoint.next = point.next;
size--;
return point.data;
}
point = point.next;
prePoint = prePoint.next;
linkLoc++;
}
return null;
}
public void prinf() {
Node<T> point = head.next;
while (point != null) {
System.out.printf("%s\t", point.data.toString());
point = point.next;
}
System.out.println();
}
public int getSize() {
return size;
}
}
MemoryFIFO.java (模拟FIFO)
package opLearn;
public class MemoryFIFO {
public static LinkList<String> linkList = new LinkList<>();
public static int pageSize = 4;
public static void main(String[] args) {
linkList.add("a");
linkList.add("b");
linkList.add("c");
linkList.add("d");
String[] pageUse = {"c", "a", "d", "b", "e", "b", "a", "b", "c", "d"};
for (String page : pageUse) {
getPage(page);
linkList.prinf();
System.out.println("==================");
}
}
public static void getPage(String page) {
int dataLoc = linkList.find(page);
// 页不存在
if (dataLoc == -1) {
if (linkList.getSize() >= pageSize) {
// 若大小大于物理页数量,缺页中断处理
String pageOut = PageFaultDeal(page);
System.out.printf("淘汰:%s 插入:%s\n", pageOut, page);
} else {
linkList.add(page);
}
}
}
public static String PageFaultDeal(String data) {
// 首页出队列
String pageOut = linkList.remove(0);
// 分配新页入队列尾
linkList.add(data);
return pageOut;
}
}
结果:
a b c d
==================
a b c d
==================
a b c d
==================
a b c d
==================
淘汰:a 插入:e
b c d e
==================
b c d e
==================
淘汰:b 插入:a
c d e a
==================
淘汰:c 插入:b
d e a b
==================
淘汰:d 插入:c
e a b c
==================
淘汰:e 插入:d
a b c d
==================
3. 最近最久未使用算法(LRU)
基本思路:当缺页中断发生时,选择最久未使用的页面,并淘汰之。
原理:LRU是对页面置换算法的一个近似,依据是程序的局部性原理。某些页面长时间未被访问,那么将来它们还可能会长时间得不到访问。
实现:LRU需要记录各个页面使用时间的先后顺序,开销较大,可能的实现方法有:
- 链表,刚刚使用的页面作为头,未使用的未尾,缺页中断时,替换链表尾
- 堆栈,每次访问是将数据压入栈顶,查询除栈顶外是否已有该数据,有则删除
缺点:需要遍历链表/堆栈,开销大
代码实现:
package opLearn;
public class MemoryLRU {
public static LinkList<String> linkList = new LinkList<>();
public static int pageSize = 4;
public static void main(String[] args) {
linkList.add("a");
linkList.add("b");
linkList.add("c");
linkList.add("d");
String[] pageUse = {"c", "a", "d", "b", "e", "b", "a", "b", "c", "d"};
for (String page : pageUse) {
getPage(page);
linkList.prinf();
System.out.println("==================");
}
}
public static void getPage(String page) {
int dataLoc = linkList.find(page);
if (dataLoc != -1) {
//此处简化,使用先删除后插入方法,实际可以通过链表指向来交换位置
linkList.remove(dataLoc);
linkList.add(page);
} else {
if (linkList.getSize() >= pageSize) {
String pageOut = PageFaultDeal();
linkList.add(page);
System.out.printf("淘汰:%s 插入:%s\n", pageOut, page);
} else {
linkList.add(page);
}
}
}
public static String PageFaultDeal() {
// 删除最久未被访问页
return linkList.remove(0);
}
}
结果:
a b d c
==================
b d c a
==================
b c a d
==================
c a d b
==================
淘汰:c 插入:e
a d b e
==================
a d e b
==================
d e b a
==================
d e a b
==================
淘汰:d 插入:c
e a b c
==================
淘汰:e 插入:d
a b c d
==================
4. 时钟页面置换算法
简介:clock页面置换算法,是lru的近似,对fifo是一种改进
基本思路:
- 需要用到页表项中的访问位,当一个页面被装入内存时,把该位初始化为0。然后如果这个页面被访问(读/写),则把该位置为1
- 把各个页面组织成环形链表,把指针指向最老的页面(最先进来)
- 当发生一个缺页中断时,考察指针所指向的最老页面,若它的访问位为0,立即淘汰;若访问位为1,则把该位置为0,然后指针往下移动一格。如此下去,直到找到被淘汰的页面,然后把指针移动到它的下一格
实现:
维持一格环形页面链表于内存中
- 用一个时钟位来标志一个页面是否经常被访问
- 当一个页面被访问时,这个位设置为1
时钟头扫描页面寻找一个时钟位为0的页面,替换没有被引用过的页面
例子:
代码实现(TODO)
缺点:效果弱于lru,但接近lru
5. 二次机会法
问题:clock页面置换算法的问题: 存在巨大代价来替换脏页(未被修改的页也需要写回)
优化:
- 允许脏页总是在一次时钟头扫描中保留下来,同时使用"脏位"和"使用位"来指导替换,减少脏页被置换的次数,进而减少磁盘写操作
实现:类似clock算法,但替换操作由"脏页位"和"操作位"同时控制
例子:
代码实现(TODO)
6. 最不常用法(LFU least frequently used)
基本思路: 当一个缺页中断发生时,选择访问次数最少的那个页面,并淘汰之
实现方法:对每个页面设置一个访问计数器,每当一个页面被访问时,该页面访问计数器加1,在发生缺页中断时,淘汰计数器最小的页面(解决方法:考虑时间信息,隔一段时间对计数器数值进行衰减)
LRU和LFU区别:LRU考察的是多久未访问,访问时间距离当前时间越近越好;LFU考虑访问次数,页面被访问的频率越高越好。
问题:
- 实现效率:淘汰计数最小的页面时,需要遍历页面,开销大
- 硬件成本:需要增加计数器
- LRU考虑的是访问次数,若程序初始化时,对某部分数据或代码段访问频繁,而初始化后访问次数较少,这个情况会导致LRU缺页中断置换页面次数增多。
代码实现(TODO)
例子:
三、Belady现象、LRU、FIFO、clock对比
1. Belady现象
belady现象:在采用FIFO算法时,有时会出现分配的物理页面数增加,缺页率反而提高的异常现象
例子:
解决方法:lru
2. LRU、FIFO和CLOCK算法比较
LRU和FIFO本质是先进先出的思路,只不过LRU针对页面的最近访问时间来进行排序,所以需要在每一次页面访问的时候动态调整页面先后顺序;FIFO则只针对页面进入内存的时间进行排序,这个时间是固定不变的,所以页面之间先后顺序是固定的。如果一个页面进入内存后一直没有被访问,那其最近访问时间就是它进入内存的时间,因此,如果所有页面只在进入内存时被访问,那么LRU算法便退化为FIFO算法(clock同理)。clock算法是lru和FIFO算法的折中算法
四、全局置换算法
1. 局部置换算法的问题和工作集模型
背景:多道程序设计,各个程序内存页如何分配
工作集模型:一个进程当前页面正在使用的逻辑页面集合,可用一个二元函数W(t,delta)来表示,其中t是当前执行时刻,delta是工作集窗口,即一个定长页面访问的时间窗口,
W(t,delta)=在当前时刻t之前,delta窗口当中的所有页面所组成的集合
|W(t,delta)|=工作集大小,即页面数量
工作集例子:
工作集特点: 进程开始执行后,随着访问新页面逐步建立较稳定的工作集。当内存访问的局部性区域的位置大致稳定时,工作集大小也大致稳定;局部性区域位置改变时,工作集快速扩张和收缩过渡到下一个稳定值。
常驻集:当前时刻,进程实际驻留在内存当中的页面集合
工作集与常驻集的区别和联系:
- 工作集是进程在运行过程中固有的性质,常驻集取决于系统分配给进程的物理页面数目,以及所采用的页面置换算法
- 若一个进程整个工作集都在内存中(常驻集包含工作集),那进程运行顺利,不会造成太多的缺页中断
- 当进程的常驻集大小达到某个数目之后,给它分配更多的物理页面,缺页率也不会明显下降
2. 两个全局页置换算法
a. 工作集页置换算法
思路:追踪前n个内存访问页引用,形成工作集窗口,工作集窗口里的页代表当前一段时间内被访问的页,页替换策略:不在工作集的页
b. 缺页率页面置换算法
可变分配策略:常驻集大小可变,先根据程序大小分配一定数量物理页面,然后在进程运行期间,动态调整常驻集大小
思路:可采用全局页面置换的方式,当发生一个缺页中断时,被置换的页面可以是在其他进程中,各个并发进程竞争的使用物理页面。
优缺点:性能较好,但增加了系统开销
影响缺页率的因素:页面置换算法/分配给进程的物理页面数目/页面本身大小/程序编写
具体实现:
- 可以使用缺页率算法(PFF)来动态调整常驻集的大小
- 若发生页缺失的时间间隔较大(大于设定的阈值),则减少工作集
- 若发生页缺失的时间间隔较小(小于阈值),则增加工作集
例子:
与工作集页置换算法的异同:
- 调整页数量的时机不同:工作集页置换算法在每一次访问时都需要判断,而缺页率置换算法则在每一次页替换后才判断
- 工作集页置换算法和缺页率置换算法均动态更改页大小
3. 抖动问题
a. 简介
定义:如果分配给一个进程的物理页面太少,不能包含整个工作集(常驻集属于工作集),那么进程会造成很多缺页中断,需要频繁的内存和外存之间替换页面,从而使进程的运行速度变慢,这种现象称为“抖动”。
产生抖动的原因:随着驻留内存的进程数目增加,分配给每个进程的物理页面数不断减小,缺页率不断上升。所以os要选择一个适合的进程数目和进程需要的帧数,以便在并发水平和缺页率之间达到平衡。
b. 抖动和负载控制:
MTBF(平均页缺失时间):平均缺页间隔时间
一台计算机中,当程序数量超过一定阈值,由于频繁发生页面置换,cpu大部分时间被操作系统用于替换内存页,cpu利用率会下降,因此需要找到最优值(两次缺页的间隔时间与缺页异常处理的时间(PFST)相等处,此时并发度最高,且页面置换影响较小)