OPT、FIFO、LRU算法的实现
一、实验目的
- 了解虚拟存储技术的特点,掌握虚拟存储请求页式存储管理中几种基本页面置换算法的基本思想和实现过程,并比较它们的效率。
- 了解程序设计技术和内存泄露的原因
二、实验内容
模拟实现请求页式存储管理的几种基本页面置换算法
- 最佳淘汰算法(OPT)
- 先进先出的算法(FIFO)
- 最近最久未使用算法(LRU)
三、 实验原理
1. 虚拟存储系统
UNIX中,为了提高内存利用率,提供了内外存进程对换机制;内存空间的分配和回收均以页为单位进行;一个进程只需将其一部分(段或页)调入内存便可运行;还支持请求调页的存储管理方式。
当进程在运行中需要访问某部分程序和数据时,发现其所在页面不在内存,就立即提出请求(向CPU发出缺中断),由系统将其所需页面调入内存。这种页面调入方式叫请求调页。
为实现请求调页,核心配置了四种数据结构:页表、页框号、访问位、修改位、有效位、保护位等。
2. 页面置换算法
当CPU接收到缺页中断信号,中断处理程序先保存现场,分析中断原因,转入缺页中断处理程序。该程序通过查找页表,得到该页所在外存的物理块号。如果此时内存未满,能容纳新页,则启动磁盘I/O将所缺之页调入内存,然后修改页表。如果内存已满,则须按某种置换算法从内存中选出一页准备换出,是否重新写盘由页表的修改位决定,然后将缺页调入,修改页表。利用修改后的页表,去形成所要访问数据的物理地址,再去访问内存数据。整个页面的调入过程对用户是透明的。
- 最佳淘汰算法(OPT):选择永不使用或在未来最长时间内不再被访问的页面予以替换。
- 先进先出的算法(FIFO):选择在内存中驻留时间最久的页面予以替换。
- 最近最久未使用算法(LRU):选择过去最长时间未被访问的页面予以替换。
3. 首先用srand( )和rand( )函数定义和产生指令序列,然后将指令序列变换成相应的页地址流,并针对不同的算法计算出相应的命中率。
(1)通过随机数产生一个指令序列,共320条指令。指令的地址按下述原则生成:
A:50%的指令是顺序执行的
B:25%的指令是均匀分布在前地址部分
C:25%的指令是均匀分布在后地址部分
具体的实施方法是:
A:在[0,319]的指令地址之间随机选取一起点m
B:顺序执行一条指令,即执行地址为m+1的指令
C:在前地址[0,m+1]中随机选取一条指令并执行,该指令的地址为m’
D:顺序执行一条指令,其地址为m’+1
E:在后地址[m’+2,319]中随机选取一条指令并执行
F:重复步骤A-E,直到320次指令
(2)将指令序列变换为页地址流
设:页面大小为1K;
用户内存容量4页到32页;
用户虚存容量为32K。
在用户虚存中,按每K存放10条指令排列虚存地址,即320条指令在虚存中的存放方式为:
第 0 条-第 9 条指令为第0页(对应虚存地址为[0,9])
第10条-第19条指令为第1页(对应虚存地址为[10,19])
………………………………
第310条-第319条指令为第31页(对应虚存地址为[310,319])
按以上方式,用户指令可组成32页。
四、实验中用到的系统调用函数
因为是模拟程序,可以不使用系统调用函数。
五、程序流程图
-
生成随机指令并转换为页地址流
-
OPT算法
-
OPT算法中找到换出页面的方法
-
FIFO算法
-
LRU算法
文字补充:
流程图是这几种方法的思路,具体实现还有很多细节,细节在代码注释
代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//内存
//作为链表节点,存放页面
typedef struct PageInMemory {
int pageIndex;
struct PageInMemory* next;
} memory;
//生成begin~rear随机数
int myrandom(int begin, int end) {
if (begin == 0) return rand() % (end + 1);
else return rand() % (end - begin + 1) + begin;
}
//生成指令序列
void makeInstructionSequence(int* instructionSequence) {
//在[0,319]的指令地址之间随机选取一起点m
//第一个起点小于319,因为后面要顺序执行的话,320会超出[0,319]的范围
int m;
//save在前地址选取指令中用于存储m
int save;
m = myrandom(0, 318);
for (int i = 0; i < 320; i += 4) {
instructionSequence[i] = m;
//顺序执行一条指令,即执行地址为m+1的指令
instructionSequence[i + 1] = m + 1;
//在前地址[0,m+1]中随机选取一条指令并执行,该指令的地址为m’
//小于317,因为后面要顺序执行和后地址选指令的话,有可能出现320,会超出[0,319]的范围
save = m + 1;
do {
m = myrandom(0, save);
} while (m >= 317);
instructionSequence[i + 2] = m;
//顺序执行一条指令,其地址为m’ + 1
instructionSequence[i + 3] = m + 1;
//在后地址[m’+2,319]中随机选取一条指令并执行
//小于319,因为后面要顺序执行的话,319顺序执行,会出现320,超出[0,319]的范围
m = myrandom(m + 2, 318);
}
}
//指令序列变换为页地址流
void instructionToPageAdressStream(int* page, int* instructionSequence) {
for (int i = 0; i < 320; i++) page[i] = instructionSequence[i] / 10;
}
//OPT算法中找到要被换出的页面
//用一个链表复制当前系统内存链表
//然后从传进来的指令开始,往后寻找在复制链表中出现的指令页面
//删除复制链表至只剩下一个结点或者走完全部指令序列
//返回剩下的某个结点即可
//该结点即为永不使用或在未来最长时间内不再被访问的页面
int match(int* page, int index, memory* front, int memoryCapacity) {
//构造复制内存链表
//frontSimulation作为复制内存链表的头部
//头结点
memory* new = ((memory*)malloc(sizeof(memory)));
new->pageIndex = front->pageIndex;
new->next = NULL;
//curr为复制内存链表的修改指针,p为原内存链表的当前指针
memory* frontSimulation = new, * curr = frontSimulation, * p = front->next;
while (p) {
new = ((memory*)malloc(sizeof(memory)));
new->pageIndex = p->pageIndex;
new->next = NULL;
curr->next = new;
curr = curr->next;
p = p->next;
}
//计算已经删除的复制内存链表结点数
int count = 0;
for (int i = index; i < 320; i++) {
//如果链表头等于循环中的页,直接删除链表头
if (frontSimulation->pageIndex == page[i]) {
memory* save = frontSimulation;
frontSimulation = frontSimulation->next;
free(save);
count++;
//如果删除至1个,则直接返回该结点指针
if (memoryCapacity - count == 1) return frontSimulation->pageIndex;
continue;
}
//如果链表头不等于循环中的页,则逐个寻找
//pre为需删除结点的前一个结点
//因为已经判断了链表头,所以curr直接从链表头的下一个结点开始
memory* pre = frontSimulation;
curr = frontSimulation->next;
//用于判断有没有找到需删除的结点,即与当前循环页相同的的结点
int flag = 0;
while (curr) {
if (curr->pageIndex == page[i]) {
flag = 1;
break;
}
pre = pre->next;
curr = curr->next;
}
//找到需要删除的结点
if (flag) {
pre->next = curr->next;
free(curr);
count++;
//如果删除至1个,则直接返回该结点指针
if (memoryCapacity - count == 1) return frontSimulation->pageIndex;
}
//没找到需要删除的结点
else continue;
}
//若没有在上面的循环中返回,即说明已走完了指令序列
//只需随意选择一个剩余结点返回即可
return frontSimulation->pageIndex;
}
//最佳淘汰算法(OPT)
//用链表
void OPT(int memoryCapacity, int* page) {
//命中率
double hitRate;
//没命中的页面数,初始化为0
int miss = 0;
//内存队列的头和尾
memory* front, * rear;
front = rear = NULL;
for (int i = 0; i < 320; i++) {
memory* curr = front;
//flag用于判断该指令所在页面是否在内存
int flag = 0;
//count统计链表长度,若该页面不在内存中,用于更新指针front
int count = 0;
while (curr) {
if (curr->pageIndex == page[i]) {
flag = 1;
break;
}
count++;
curr = curr->next;
}
//指针不再使用,置空
curr = NULL;
//指令所在页面在内存中,继续下一条指令
if (flag) continue;
//指令所在页面不在内存,按先进先出的原则,将所需页面调入内存
else {
miss++;
//创建新结点
memory* new = ((memory*)malloc(sizeof(memory)));
if (new == NULL) {
printf("内存分配不成功!\n");
exit(1);
}
new->pageIndex = page[i];
new->next = NULL;
//如果内存中没有页面,相当于初始化front
if (count == 0) front = new;
//如果内存已满
else if (count == memoryCapacity) {
//找到要换出的结点的指针
int replace = match(page, i, front, memoryCapacity);
//如果队列头要换出,更新队列头,删除原来的队列头结点
if (replace == front->pageIndex) {
memory* save = front;
front = front->next;
free(save);
}
//如果不是队列头要换出
//则要找到要换出结点的前一个结点
else {
memory* pre = front, * curr = front->next;
while (curr->pageIndex != replace) {
pre = pre->next;
curr = curr->next;
}
pre->next = curr->next;
free(curr);
rear = front;
while (rear->next) rear = rear->next;
}
rear->next = new;
}
//如果内存中已有页面,将新结点连在队列最后
else rear->next = new;
//rear调整为最后
rear = new;
}
}
//释放资源
while (front) {
memory* curr = front;
front = front->next;
free(curr);
curr = NULL;
}
rear = NULL;
//计算命中率
hitRate = 1 - miss / 320.0;
printf("OPT: %f%s\t\t", hitRate * 100, "%");
}
//先进先出的算法(FIFO)
//用队列方法实现
void FIFO(int memoryCapacity, int* page) {
//命中率
double hitRate;
//没命中的页面数,初始化为0
int miss = 0;
//内存队列的头和尾
memory* front, * rear;
front = rear = NULL;
for (int i = 0; i < 320; i++) {
memory* curr = front;
//flag用于判断该指令所在页面是否在内存
int flag = 0;
//count统计链表长度,若该页面不在内存中,用于更新指针front
int count = 0;
while (curr) {
if (curr->pageIndex == page[i]) {
flag = 1;
break;
}
count++;
curr = curr->next;
}
//指针不再使用,置空
curr = NULL;
//指令所在页面在内存中,继续下一条指令
if (flag) continue;
//指令所在页面不在内存,按先进先出的原则,将所需页面调入内存
else {
miss++;
//创建新结点
memory* new = ((memory*)malloc(sizeof(memory)));
if (new == NULL) {
printf("内存分配不成功!\n");
exit(1);
}
new->pageIndex = page[i];
new->next = NULL;
//如果内存已满
//释放队列头第一个结点,front调整
if (count == memoryCapacity) {
memory* curr = front;
if (front) front = front->next;
free(curr);
curr = NULL;
rear->next = new;
}
//如果内存中没有页面,相当于初始化front
if (count == 0) front = new;
//如果内存中已有页面,将新结点连在队列最后
else rear->next = new;
//rear调整为最后
rear = new;
}
}
//释放资源
while (front) {
memory* curr = front;
front = front->next;
free(curr);
curr = NULL;
}
rear = NULL;
//计算命中率
hitRate = 1 - miss / 320.0;
printf("FIFO: %f%s\t", hitRate * 100, "%");
}
//最近最久未使用算法(LRU)
//用栈方法实现
void LRU(int memoryCapacity, int* page) {
//命中率
double hitRate;
//缺页数,初始化为0
int miss = 0;
//内存栈的栈顶和栈底
memory* top, * bottom;
top = bottom = NULL;
for (int i = 0; i < 320; i++) {
memory* curr = bottom;
//flag用于判断该指令所在页面是否在内存
int flag = 0;
//count统计链表长度,若该页面不在内存中,用于更新指针front
int count = 0;
while (curr) {
if (curr->pageIndex == page[i]) {
flag = 1;
break;
}
count++;
curr = curr->next;
}
//指令所在页面在内存中
//将该页面从栈中取出,放在栈顶上
if (flag) {
//如果该页面在栈底
if (bottom == curr) {
top->next = bottom;
bottom = bottom->next;
top = top->next;
top->next = NULL;
}
//如果该页面不在栈底
else {
//pre为curr的前一个结点的指针
memory* pre = bottom;
while (pre->next != curr) pre = pre->next;
top->next = curr;
pre->next = curr->next;
top = top->next;
top->next = NULL;
}
//继续下一条指令
continue;
}
//指令所在页面不在内存,按先进先出的原则,将所需页面调入内存
else {
//缺页数+1
miss++;
//创建新结点
memory* new = ((memory*)malloc(sizeof(memory)));
if (new == NULL) {
printf("内存分配不成功!\n");
exit(1);
}
new->pageIndex = page[i];
new->next = NULL;
//如果内存中没有页面,相当于初始化top和bottom
if (count == 0) {
top = new;
bottom = new;
}
//如果内存已满
//删除栈底元素
//在将新结点放在栈顶
else if (count == memoryCapacity) {
memory* curr = bottom;
if (bottom) bottom = bottom->next;
free(curr);
top->next = new;
top = top->next;
top->next = NULL;
}
//如果内存中已有页面,将新结点放在栈顶
else {
top->next = new;
top = top->next;
top->next = NULL;
}
}
}
//释放资源
while (bottom) {
memory* curr = bottom;
bottom = bottom->next;
free(curr);
curr = NULL;
}
top = NULL;
//计算命中率
hitRate = 1 - miss / 320.0;
printf("LRU: %f%s\t", hitRate * 100, "%");
}
int main() {
srand((unsigned)time(NULL));
//指令序列
int* instructionSequence = (int*)malloc(sizeof(int) * 320);
makeInstructionSequence(instructionSequence);
//将指令序列变换为页地址流
//页号
//模拟算法不需要偏移量,并且题目没有要求
int* page = (int*)malloc(sizeof(int) * 320);
instructionToPageAdressStream(page, instructionSequence);
for (int i = 0; i < 320; i++) printf("%d ", page[i]);
printf("\n");
//用户内存容量从4页到32页
for (int i = 4; i <= 32; i++)
{
printf("用户内存容量为%d页\n", i);
OPT(i, page);
FIFO(i, page);
LRU(i, page);
printf("\n\n");
}
//释放资源
free(page);
page = NULL;
free(instructionSequence);
instructionSequence = NULL;
return 0;
}
运行结果(百分数为命中率)
留意用户内存容量为4页和32页即可
第一次
第二次
第三次
分析
-
用户内存容量越大,命中率越高,三种方法的命中率趋于相同。原因是内存容量越大,存入内存中的页面就越多,当大部分甚至全部页面都存进内存后,基本上不会发生缺页;
-
无论内存容量多大,使用哪种方法,命中率都无法达到100%。原因是进程开始运行时一定会发生缺页;
-
当内存容量较低时,fifo和lru两种方法的命中率都是50%左右。原因是,页地址流如下图,指令序列为顺序->前地址->顺序->后地址这样重复,从而得到的页地址流。因为这样得到的页地址流基本上是两两成对,每组第一个指令调入后,第二个指令大多数情况下不会缺页;
个人理解题目要求的指令序列就是这样生成的
-
fifo方法没有存在Belady异常现象,即命中率随内存块增加而减少。原因是按题目要求生成的页地址流有两两成对的规律,不是理论情况下的完全乱序页地址流;
-
在内存容量较低时,opt方法的命中率比其余两种算法都高。原因是opt方法的实质是淘汰的老页面是将来不被使用或者在最远的将来才被使用,所以命中率最高,并且随着内存容量增大,opt方法的命中率最快达到峰值;
-
当内存容量达到32页时,三种方法的命中率都相同并且>=90%。原因是页地址流总共就只有32页或者小于32页,当全部页面页都放进内存中,就不会发生缺页了。