一、實驗目的
- 了解虛擬存儲技術的特點,掌握虛擬存儲請求頁式存儲管理中幾種基本頁面置換算法的基本思想和實現過程,並比較它們的效率。
- 了解程序設計技術和內存泄露的原因
二、實驗內容
模擬實現請求頁式存儲管理的幾種基本頁面置換算法
- 最佳淘汰算法(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頁,當全部頁面頁都放進內存中,就不會發生缺頁了。