一、实验目的
1)理解页面淘汰算法原理,编写程序演示页面淘汰算法。
2)验证Linux虚拟地址转化为物理地址的机制
3)理解和验证程序运行局部性的原理。
二、实验内容
1)Win/Linux编写二维数组遍历程序,理解局部性的原理。
2)Windows/Linux模拟实现OPT和LRU等淘汰算法。
3)Linux下利用/proc/pid/pagemap技术计算某个变量或函数虚拟地址对应的物理地址等信息。
提示1:数组尽可能开大一些,并尝试改变数组大小,改变内外重循环次序。例如从[2048] X[2048]变化到[10240] x [20480],观察它们的遍历效率。
提示2:在任务管理中观察它们的缺页次数。
#include
#include
#include
using namespace std;
int MyArray[10240][20480];
const string str[4] = { "局部性好","局部性差","2048*2048","10240*20480" };
int main() {
DWORD pid = GetCurrentProcessId();
cout << "当前进程的PID是"<<pid<<",您可以根据进程号在任务管理器查看进程。" << endl;
cout << "模式(1. 局部性好;2. 局部性差)" << endl;
cout << "数组大小(1. 2048*2048;2. 10240*20480)" << endl << endl;
int op1 = 0, op2 = 0;
while (1)
{
cout << "******* INPUT 2 INT TO USE *******" << endl;
cin >> op1 >> op2;
cout << "您选择了组合("<<str[op1-1]<<","<<str[op2+1]<<"),运行时间为:";
int sizex=1, sizey=1;
if (op2 == 1)
{
sizex = sizey = 2048;
}
else if (op2 == 2)
{
if (op1 == 1) {
sizex = 10240;
sizey = 20480;
}
else if (op1 == 2)
{
sizey = 10240;
sizex = 20480;
}
}
clock_t start, end;
start = clock();
for (int i = 0; i < sizex; i++)
for (int j = 0; j < sizey; j++) {
if (op1 == 1) {
MyArray[i][j] = 0;
}
else if(op1==2){
MyArray[j][i] = 0;
}
}
end = clock();
double time1 = (double)(end - start) / CLOCKS_PER_SEC;
cout << time1 << "秒" << endl << endl;
}
return 0;
}
1)任务平台:Windows 10, Visual Studio 2019。
2)如何观察缺页次数:在任务管理器-详细信息中,右键列名,“选择列”,添加“页面错误”。
3)实验变量:
①程序局部性;②页面是否被调入内存。
4)实验前猜测:
①访问数组时会将部分页面调到内存中,占用内存增加、页面错误增加。
②局部性差的情况需要消耗更多的时间。
③局部性好坏不影响缺页次数。
④页面被调入内存后,再次访问,效率会提高。
5)实验过程:
由于刚才访问时数组的内容尚未调入内存,为保证变量唯一,再次运行(小数组,局部性好)。虽然时间减少了,但是由于数组较小,随机性较大,不能证明猜测④。
接着再运行(小数组,局部性差)。
可见局部性差时时间增加,猜测②得证。
并且不论局部性好坏,页面错误和活动的内存均未发生改变,因此局部性好坏不影响页面错误。猜测③得证。
由于小数组的时间的偶然性较大,使用大数组重复运行,可见调入内存后,再次访问的速度明显加快。下图1是未调入时,下图2、3是调入时,猜测④得证。
[以下模拟过程仅供参考,不是唯一方案!百度参考其他方案!]
提示1:程序指令执行过程采用遍历数组的操作来模拟;
提示2:用1个较大数组A(例如2400个元素)模拟进程,数组里面放的是随机数,每个元素被访问时就使用printf将其打印出来,模仿指令的执行。数组A的大小必须是设定的页大小(例如10个元素或16个元素等等)的整数倍。
提示3:用3-8个小数组(例如数组B,数组C,数组D等)模拟分得的页框。小数组的大小等于页大小(例如10条指令的大小,即10的元素)。小数组里面复制的是大数组里面的相应页面的内容(自己另外构建页表,描述大数组的页与小数组序号的关系)。
提示4:利用不同的次序访问数组A,次序可以是:顺序,跳转,分支,循环,或随机,自己构建访问次序。不同的次序也一定程度反映程序局部性。
提示5:大数组的访问次序可以用 rand( )函数定义,模拟产生指令访问序列,对应到大数组A的访问次序。然后将指令序列变换成相应的页地址流,并针对不同的页面淘汰算法统计“缺页”情况。缺页即对应的“页面”没有装到小数组中(例如数组B,数组C,数组D等)。
提示6:实验中页面大小,页框数,访问次序,淘汰算法都应可调。
提示7:至少实现2个淘汰算法。
为了方便浏览提示,我做了一点颜色标记,如下。
每种淘汰模式:
淘汰算法:
很长,不贴了
#include
#include
#include
#include
#pragma warning(disable:4996)
using namespace std;
#define pageTotalSize 2400
#define seqLength 800
int pageSize = 10; //页面大小
int pageFrameNum = 3; //页框数
int visitOrder = 0; //访问次序
int eliminateAlgorithm = 0; //淘汰算法
string eliminateStr[4] = {"FIFO","OPT","LRU","LFU"};
string visitStr[4] = { "顺序","跳转","循环","随机"};
int page[pageTotalSize];
vector<int>pageFrame[20]; //页框数最大20
int visitSeq[seqLength];
void FIFO(int seq[seqLength], bool showDetail); /*其所选择的被淘汰的页面是最早的一页*/
void OPT(int seq[seqLength], bool showDetail); /*其所选择的被淘汰的页面将是以后永不使用的,或是在最长(未来)时间内不再被访问的页面*/
void LRU(int seq[seqLength], bool showDetail); /*根据数据的历史访问记录来进行淘汰数据*/
void LFU(int seq[seqLength], bool showDetail); /*根据数据的历史访问频率来进行淘汰数据*/
void Order(int visitOrder); /*生成指定次序的访问数组*/
测试了很多次,数据少有时候跑出来的结果LRU比OPT还好,它真的是一个很优秀的算法。
提示1:Linux的/proc/pid/pagemap文件允许用户查看当前进程虚拟页的物理地址等相关信息。
提示2:获取当前进程的pagemap文件的全名
提示3:可以输出进程中某个或多个全局变量或自定义函数的虚拟地址,所在页号,所在物理页框号,物理地址等信息。
思考:(1)如何通过扩充实验展示不同进程的同一虚拟地址对应不同的物理地址。(2)如何通过扩充实验验证不同进程的共享库具有同一的物理地址。
我觉得这篇博客的代码写得比我写的好,分析也很好,但是因为写得太好了不像我能写出来的代码,所以我主要没有借鉴它的代码。利用/proc/pid/pagemap将虚拟地址转换为物理地址
#include
#include
#include
#include
#include
#include
#include
//注意用sudo运行!!!
char buf[200];
//计算虚拟地址对应的地址,传入虚拟地址vaddr
void mem_addr(char* str, unsigned long pid, unsigned long vaddr, unsigned long* paddr)
{
int pageSize = getpagesize();//调用此函数获取系统设定的页面大小
unsigned long v_pageIndex = vaddr / pageSize;//计算此虚拟地址相对于0x0的经过的页面数
unsigned long v_offset = v_pageIndex * sizeof(uint64_t);//计算在/proc/pid/page_map文件中的偏移量
unsigned long page_offset = vaddr % pageSize;//计算虚拟地址在页面中的偏移量
uint64_t item = 0;//存储对应项的值
sprintf(buf, "%s%lu%s", "/proc/", pid, "/pagemap");
//printf("%s\n",buf);
int fd = open(buf, O_RDONLY);//以只读方式打开/proc/pid/page_map
lseek(fd, v_offset, SEEK_SET);//将游标移动到相应位置
read(fd, &item, sizeof(uint64_t));//读取对应项的值,并存入item中
//printf("%lu\n",v_offset);
uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;//计算物理页号,即取item的bit0-54
*paddr = (phy_pageIndex * pageSize) + page_offset;//再加上页内偏移量就得到了物理地址
printf("【%s】pid = %lu, 虚拟地址 = 0x%lx, 所在页号 = %lu, 物理地址 = 0x%lx, 所在物理页框号 = %lu\n", str, pid, vaddr, v_pageIndex, *paddr, phy_pageIndex);
sleep(1);
}
const int a = 100;//全局常量
int main()
{
int b = 100;//局部变量
static int c = 100;//局部静态变量
const int d = 100;//局部常量
unsigned long phy = 0;//物理地址
char *p = (char*)malloc(100);//动态内存
int pid = fork();//创建子进程
mem_addr("全局常量", getpid(), (unsigned long)&a, &phy);
mem_addr("局部变量", getpid(), (unsigned long)&b, &phy);
mem_addr("局部静态变量", getpid(), (unsigned long)&c, &phy);
mem_addr("局部常量", getpid(), (unsigned long)&d, &phy);
sleep(1);
free(p);
waitpid();
return 0;
}
注意,运行时一定要加上sudo。
如图,输出了进程中多个变量的虚拟地址、所在页号、物理地址、所在物理页框号。
(1)如何通过扩充实验展示不同进程的同一虚拟地址对应不同的物理地址。
答:通过fork创建不同的进程,如图所示,pid为5022的为父进程,pid为5023的为子进程。其中全局变量的虚拟地址和物理地址,在父子进程中一致。局部变量、局部常量、局部静态变量则均不一致。
(2)如何通过扩充实验验证不同进程的共享库具有同一的物理地址。
这篇博客里有提到共享库怎么判断,我借鉴了一下,发现子进程的会显示没有present,不过其他进程是正常的。利用/proc/pid/pagemap将虚拟地址转换为物理地址
下图中包括三个进程,进程6944创建了子进程6945(右下角),以及进程6883(左下角)。
查看它们的/proc/pid/maps可见,它们都调用了同一个动态库/usr/lib/x86_64-linux-gnu/libc-2.31.so,并可见在不同进程中这个库的虚拟地址。
基于/proc/pid/pagemap获取物理地址的思路,我们修改之前的程序如下:
#include
#include
#include
#include
#include
#include
#include
//注意用sudo运行!!!
char buf[200];
void mem_addr(unsigned long pid, unsigned long vaddr, unsigned long* paddr)
{
int pageSize = getpagesize();
unsigned long v_pageIndex = vaddr / pageSize;
unsigned long v_offset = v_pageIndex * sizeof(uint64_t);
unsigned long page_offset = vaddr % pageSize;
uint64_t item = 0;
sprintf(buf, "%s%lu%s", "/proc/", pid, "/pagemap");
int fd = open(buf, O_RDONLY);
lseek(fd, v_offset, SEEK_SET);
read(fd, &item, sizeof(uint64_t));
uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;
*paddr = (phy_pageIndex * pageSize) + page_offset;
printf("pid = %lu, 虚拟地址 = 0x%lx, 所在页号 = %lu, 物理地址 = 0x%lx, 所在物理页框号 = %lu\n", pid, vaddr, v_pageIndex, *paddr, phy_pageIndex);
sleep(1);
}
int main(int argc , char* argv[])
{
unsigned long phy = 0;//物理地址
printf("pid = %lu",getpid());
mem_addr(atoi(argv[1]), (unsigned long)strtol(argv[2],NULL,16), &phy);
sleep(1000);
return 0;
}
使其允许接受命令行参数,然后用它检测不同进程的虚拟地址对应的物理地址。如下图所示。
可见,不同进程的共享库具有同一的物理地址。
而子进程的共享库好像不见了(bushi)。