虚拟内存页面置换
内存是计算机最为珍贵的资源,其可被CPU直接访问且速度较快,但正因为如此,严重限制了其存储容量。而进程实际占用的存储空间一般都远大于内存空间,因此我们必须时时刻刻将内存中暂不需要的页面置换出内存,并将需要被执行的程序片段调入内存以确保程序的正常执行,这也就是虚拟内存的基本原理。而好的页面置换算法也直接决定了计算机的性能。在这一模块当中,我们来实现几种最经典的页面置换算法。
(1)时钟算法
时钟算法可以理解为LRU算法的一种近似,其基本思想是:需要用到页表项当中的访问位,当一个页面被装入内存时,把该位初始化为0,然后如果这个页面被访问(读/写),则把该位置为1。把各个页面组织成环形链表(类似于钟表面),把指针指向最老的页面(最先进来)。当发生一个缺页中断时,考察指针所指向的最老页面,若它的访问位为0,立即淘汰;若访问位为1,则把该位置为0,然后指针往下移动一格,如此下去,直至找到被淘汰的页面,然后把指针移动到它的下一格。
我们新建一个clock.c文件来实现时钟,代码如下:
首先,我们先定义并初始化一些重要变量:
#define Maxblocks 3
int blocks[Maxblocks] = {};
int access[Maxblocks] = {};
int modify[Maxblocks] = {};
int pages_in_blocks = 0;
int is_modified = 0;
int aimed_i=-1;
bool is_aimed(int pn)
{
aimed_i=-1;
for(int i=0;i<pages_in_blocks;i++)
{
if(blocks[i]==pn)
{
aimed_i=i;
return true;
}
}
return false;
}
我们先定义最大物理帧数为3,然后相继定义物理帧,访问位,修改位。然后定义一个is_aimed函数来获取并确定置换状态,如果正常的化返回true值。
void loop()
{
int taotaip=0;
for(int j=0;j<2;j++)
{
for(int i=0;i<pages_in_blocks;i++)
{
if(!access[i]&&!modify[i])
{
taotaip=blocks[i];
for(;i<pages_in_blocks;i++)
{blocks[i]=blocks[i+1];access[i]=access[i+1];modify[i]=modify[i+1];}
if(!j) printf("第一圈有(0,0)的,是页面 %d,淘汰!把新页面放入队尾\n",taotaip);
else printf("第三圈有(0,0)的,是页面 %d,淘汰!把新页面放入队尾\n",taotaip);
return;
}
}
if(!j) printf("第一圈没有(0,0)的,接着找(0,1)的\n");
else printf("第三圈没有(0,0)的,即上一圈(第二圈)全为(0,1)\n");
for(int i=0;i<pages_in_blocks;i++)
{
if(!(!access[i]&&modify[i]))
{
access[i]=0;
}
else
{
taotaip=blocks[i];
for(;i<pages_in_blocks;i++)
{blocks[i]=blocks[i+1];access[i]=access[i+1];modify[i]=modify[i+1];}
if(!j) printf("第二圈有(0,1)的,是页面 %d,淘汰!第一个(0,1)之前的页面访问位全部置为0,并把新页面放入队尾\n",taotaip);
else printf("第四圈必有(0,1)的,将第一个(0,1)的,即页面%d淘汰!把新页面放入队尾\n",taotaip);
return;
}
}
printf("第二圈没有(0,1)的,但把所有访问位置为0了,接着第三圈找(0,0)的\n");
}
}
void clock_pro(int pn)
{
if(pages_in_blocks<Maxblocks)
{
if(is_aimed(pn))
{
access[aimed_i]=1;
printf("输入页面是否被修改%d (0/1)\n",pn);
scanf("%d",&is_modified);
if(is_modified) modify[aimed_i]=1;
}
else
{
blocks[pages_in_blocks]=pn;
access[pages_in_blocks]=1;
modify[pages_in_blocks]=0;
pages_in_blocks++;
}
}
else
{
if(is_aimed(pn))
{
access[aimed_i]=1;
printf("输入页面是否被修改%d (0/1)\n",pn);
scanf("%d",&is_modified);
if(is_modified) modify[aimed_i]=1;
}
else
{
loop();
blocks[Maxblocks-1]=pn;
access[Maxblocks-1]=1;
modify[Maxblocks-1]=0;
}
}
}
在这一部分中,我们实现具体的循环置换算法。从指针的当前位置开始,扫描帧缓冲区。在这次扫描过程中,对使用位不做任何修改。选择遇到的第一个帧(u=0, m=0)用于替换。
如果第1)步失败,则重新扫描,查找(u=0, m=1)的帧。选择遇到的第一个这样的帧用于替换。在这个扫描过程中,对每个跳过的帧,把它的使用位设置成0。
如果第2)步失败,指针将回到它的最初位置,并且集合中所有帧的使用位均为0。重复第1步,并且如果有必要,重复第2步。这样将可以找到供替换的帧。
主函数:
int main()
{
for(int i=0;i<Maxblocks;i++){ blocks[i]=-1;access[i]=0;modify[i]=0;}
int n=0;
while(n>=0)
{
is_modified=0;
printf("输入此时进程对哪个页面访问,输入-1结束程序\n");
scanf("%d",&n);
if(n>=0)
{
clock_pro(n);
print_blocks();
}
}
system("pause");
return 0;
return 0;
}
执行命令gcc clock.c -o clock编译并生成可执行文件,然后执行命令./clock运行文件,用户可自行定义页面编号来实现算法。
运行结果:
(2)先进先出(FIFO)算法
这是所有虚拟内存页面置换算法中最简单的一种,即在已有物理帧中替换出最早进入当前所分配的所有物理帧的页面。以下为具体实现:
在工作目录下建立FIFO.cpp文件,代码:
int main()
{
deque<int> dq;
deque<int >::iterator pos;
int numyk,numqueye=0;
printf("请输入物理页框块数:");
scanf("%d",&numyk);
int n;
printf("\n请输入页面走向个数:");
scanf("%d",&n);
for(int i=0; i<n; i++)
{
int in;
scanf("%d",&in);
if(dq.size()<numyk)//存在空余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++) //遍历队列
if((*pos)==in)
{
flag=1;
break;
}
if(!flag) //不存在此元素
{
numqueye++;
dq.push_back(in);//放入队列
}
}
else //不存在多余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++)
if((*pos)==in)
{
flag=1;
break;
} //存在该元素
if(!flag) //不存在此元素 则置换最先进入的项
{
numqueye++;//缺页数+1
dq.pop_front();//最先进入的出队列
dq.push_back(in);//进队列
}
}
}
printf("fifo缺页次数为:%d\n",numqueye);
printf("fifo缺页中断率为:%lf\n",(double)numqueye*1.0/n);
}
注意到,因为我们要淘汰掉最早进入这一片物理帧的那一页,所以物理帧集合的工作模式为先进先出,因此在数据结构上,我们使用先进先出的队列模型deque,越先进入物理帧集合的越靠近队首,也最早被换出。
运行命令g++ FIFO.cpp -o test1编译并生成可执行文件test1,然后执行./test1运行并测试该算法用户可以根据自己的喜好自行定义物理帧数和页面走向数,并且计算缺页率。
运行结果:
3)最佳置换(OPT)算法
最佳置换算法是最为理想的一种,即其所选择的被淘汰页面,将是以后永不使用的,或许是在最长(未来)时间内不再被访问的页面。采用最佳置换算法,通常可保证获得最低的缺页率。
实现如下:
在工作目录下建立OPT.cpp文件,代码:
#include <deque>
#include <cstdio>
#include <algorithm>
using namespace std;
struct opt
{
int value;
int time;
};
const int maxn=105;
int a[maxn];
int main()
{
deque<opt> dq;
deque<opt >::iterator pos;
int numyk,numqueye=0;
printf("请输入物理页框块数:");
scanf("%d",&numyk);
int n;
printf("\n请输入页面走向个数:");
scanf("%d",&n);
for(int i=0; i<n; i++)
scanf("%d",&a[i]);
for(int i=0; i<n; i++)
{
printf("第%d个\n",i);
int in;
in=a[i];
if(dq.size()<numyk)//存在多余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++)
if((*pos).value==in)//存在元素和它相同
{
flag=1;
break;
} //存在该元素
if(!flag) //不存在此元素
{
numqueye++;
opt temp;
temp.value=in;
int f=0;
for(int j=i+1; j<n; j++)
if(a[j]==in)
{
f=1;
temp.time=j-i;
break;
}
if(!f)
temp.time=n;
dq.push_back(temp);
}
}
else //不存在多余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++)
if((*pos).value==in)
{
flag=1;
break;
} //存在该元素
if(!flag) //不存在此元素 则置换time最大的项
{
numqueye++;//缺页数+1
int m=dq.front().time;
printf("m初始值为%d\n",m);
deque<opt >::iterator mp=dq.begin();
for (pos = dq.begin(); pos != dq.end(); pos++)
{
printf("%d %d\n",(*pos).value,(*pos).time);
if((*pos).time>m)
{
printf("迭代");
mp=pos;//时间最大的元素的位置
m=(*pos).time;
}
}
opt temp;
temp.value=in;
int f=0;
dq.erase(mp);
for(int j=i+1; j<n; j++)
if(a[j]==in)
{
f=1;
temp.time=j-i;
break;
}
if(!f)
temp.time=n;
dq.push_back(temp);
}
}
//每次之后重置
for (pos = dq.begin(); pos != dq.end(); pos++)
{
printf("队列中的元素为 %d\n",(*pos).value);
int f=0;
for(int j=i+1; j<n; j++)
if(a[j]==(*pos).value)
{
f=1;
(*pos).time=j-i;
break;
}
if(!f)
(*pos).time=n;
}
}
printf("opt缺页次数为:%d\n",numqueye);
printf("opt缺页中断率为:%lf\n",(double)numqueye*1.0/n);
}
如上所示,我们先给每一个页面赋予它初始时间,并且当有新页面置换进来的时候我们要用变量time对其进行计时,然后我们需要先判断是否存在多余页框,如果不存在,我们就需要从已有的页面当中选择一个并置换出去。而在选择要被置换出去的页面时,我们依此检查各个物理帧的time,并且找出time值最大的那一个,这样我们就锁定了需要被置换出去的页面。
还有最重要的一点就是,每次当一个物理帧完成置换操作之后,time变量必须马上更新再去完成下一步的置换,否则在下一次置换发生时检查到的time就不能反映每个物理帧真实存在的时间了。
现在我们执行命令g++ OPT.cpp -o test2编译并生成可执行文件,然后运行./test2实现算法。
运行结果;
(4)最近最少使用(LRU)算法
LRU算法的基本思想是长期不被使用的数据,在未来被用到的几率也不大。因此,当数据所占内存达到一定阈值时,要移除掉最近最少使用的数据。
实现如下:
在工作目录下建立LRU.cpp,代码:
#include <deque>
#include <cstdio>
#include <algorithm>
using namespace std;
struct opt
{
int value; //值
int time; //时间
};
const int maxn=105;
int a[maxn];
int main()
{
deque<opt> dq;
deque<opt >::iterator pos;
int numyk,numqueye=0;
printf("请输入物理页框块数:");
scanf("%d",&numyk);
int n;
printf("请输入页面走向个数:");
scanf("%d",&n);
for(int i=0; i<n; i++)
scanf("%d",&a[i]);
for(int i=0; i<n; i++)
{
int in;
in=a[i];
if(dq.size()<numyk)//存在多余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++)
if((*pos).value==in)//存在元素和它相同
{
flag=1;
break;
} //存在该元素
if(!flag) //不存在此元素
{
numqueye++;
opt temp;
temp.value=in;
temp.time=0;
dq.push_back(temp);
}
}
else //不存在多余页框
{
int flag=0;
for (pos = dq.begin(); pos != dq.end(); pos++)
if((*pos).value==in)
{
flag=1;
break;
} //存在该元素
if(!flag) //不存在此元素
{
numqueye++;//缺页数+1
int m=dq.front().time;
deque<opt >::iterator mp=dq.begin();
for (pos = dq.begin(); pos != dq.end(); pos++)
{
printf("%d %d\n",(*pos).value,(*pos).time);
if((*pos).time>m)
{
printf("迭代");
mp=pos;//时间最大的元素的位置
m=(*pos).time;
}
}
printf("此时队列中所剩时间最长的元素为%d\n",(*mp).value);
opt temp;
temp.value=in;
int f=0;
dq.erase(mp);
temp.time=0; //加进来之后
dq.push_back(temp);
}
}
//每次之后各对象的time
for (pos = dq.begin(); pos != dq.end(); pos++)
{
printf("队列中的元素为 %d\n",(*pos).value);
int f=0;
for(int j=i; j>=0; j--)
if(a[j]==(*pos).value)
{
(*pos).time=i-j;
break;
}
printf("距离上次时间为%d\n",(*pos).time);
}
}
printf("lru缺页次数为:%d\n",numqueye);
printf("lru缺页中断率为:%lf\n",(double)numqueye*1.0/n);
}
如上所示,我们用一个特殊的栈来保存当前正在使用的各个页面的页面号。当一个新的进程访问某页面时,便将该页面号压入栈顶,其他的页面号往栈底移,如果内存不够,则将栈底的页面号移除。这样,栈顶始终是最新被访问的页面的编号,而栈底则是最近最久未访问的页面的页面号。
现在,我们执行命令g++ LRU.cpp -o test3编译并生成可执行文件,运行./test3实现算法,运行结果: