作業系統(十一) -- 記憶體的換入與換出及換出的基本演算法

williamgavin發表於2018-10-24

前言

前面說過為了保證記憶體在使用者程式看起來是分段,而實際是分頁的效果,引入了虛擬記憶體。對於使用者來說,虛擬記憶體是一個完整的記憶體,使用者可以隨意使用該“記憶體”,假設為4G,那麼對於使用者來說就有4G的空間可以使用,即使實際記憶體只有2G甚至1G。那麼這是如何實現的呢?這就引出了換入和換出。

換入

假設虛擬記憶體4G,實際記憶體2G。首先程式訪問0 ~ 1G這段虛擬記憶體空間的時候,就將這一段記憶體與實體記憶體建立對映。接著如果程式訪問3~4G這段虛擬記憶體空間的時候,就將這段記憶體與實體記憶體建立對映,即只有在訪問的時候才對映。

換入概述

基本流程是:一個邏輯地址CS:IP,首先根據CS在段表中找到對應的基址,加上偏移得到虛擬地址:頁號+偏移。然後根據頁號在頁表中找到對應的頁框號,加上偏移得到實體地址。但是如果在頁表中找不到對應的頁號對應的頁框地址的話,就要從磁碟上將這一頁換入了。這個換入採用的是中斷的形式,如果load[addr]的時候,發現addr在頁表裡面沒有對應對映,那麼就將中斷向量暫存器的某一位置為一,說明有中斷產生。然後在中斷服務函式裡面將addr匯入到實體記憶體中。然後再次執行load[addr]這條語句。當然查表的操作是MMU做的。

一個實際系統的請求調頁

主要是看一下是如何將某頁從磁碟換入到記憶體的,從中斷服務函式開始。cpu知道有個中斷只會就去查詢這個中斷的中斷號,然後轉去執行該中斷服務程式。這些東西是在系統初始化的時候就做好了。

void trap_init(void)
{
	set_trap_gate(14, &page_fault);
}

# define set_trap_gate(n, addr)\
	_set_gate(&idt[n], 15, 0, addr);
//在linux/mm/page.s中

.globl _page_fault
xchgl %eax,(%esp)
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10, %edx
mov %dx, %ds
mov %dx, %es
mov %dx, %fs
movl %cr2, %edx

首先push一些資訊到堆疊中(包括錯誤碼),然後mov一些東西。畢竟從使用者態到核心態需要保留一下使用者態的情況。

pushl %edx
pushl %eax
testl $1, %eax		// 測試標誌
jne 1f
call _do_no_page
jmp 2f
1: call _do_wp_page //保護
2: add $8, %esp
pop %fs
pop %es
pop %ds
pop %edx
pop %ecx
pop %eax
iret

一般push後面加了一個call呼叫c函式這種,前面push的都是壓入引數。調do_no_page();

//在linux/mm/memory.c中

void do_no_page(unsigned long error_code, unsigned long address)
{ 
	address&=0xfffff000; 				//頁面地址
	tmp=address–current->start_code; 	//頁面對應的偏移
	if(!current->executable||tmp>=current->end_data)  // 不是程式碼和資料
	{
		get_empty_page(address); 
		return; 
	}
	page=get_free_page();
	bread_page(page, current->executable->i_dev, nr);
	put_page(page, address);
	……
}

從這個函式名字也能看出 是當頁不存在的時候做的事情。
主要是這三句

	page=get_free_page();
	bread_page(page, current->executable->i_dev, nr);
	put_page(page, address);

第一句得到一個空閒頁賦值給page;第二句磁碟裡面的頁讀到記憶體中;第三句建立對映。然後再次執行load[addr]; 再次執行load[addr] 這個要硬體設計好,因為一般情況下pc指標會加一。

//在linux/mm/memory.c中
unsigned long put_page(unsigned long page, //實體地址
unsigned long address)
{ 
	unsigned long tmp, *page_table;
	page_table=(unsigned long *)((address>>20)&ffc);
	if((*page_table)&1)
	page_table=(unsigned long*)(0xfffff000&*page_table);
	else{
	tmp=get_free_page();
	*page_table=tmp|7;
	page_table=(unsigned long*)tmp;}
	page_table[(address>>12)&0x3ff] = page|7;
	return page;
}

換出

前面說了頁換入,但是記憶體並不是無限的,有換入就會有換出,換出容易,但是找到哪一頁換出不容易,所以就出現了很多替換演算法。首先應該明白替換演算法的好壞是怎麼評判的,我們希望的應該是替換次數儘可能少,什麼演算法能滿足這點就是好的演算法。下面是幾種替換演算法:

FIFO(先入先出)

即每次缺頁的時候就替換掉最開始的那一頁,這種演算法肯定在這個方面肯定不是最好的演算法,因為它沒有任何機制保證替換次數儘可能少。

MIN演算法

選擇最遠將使用的頁淘汰,什麼意思呢?假設這個程式只分配了三個頁框,現在分別儲存了A、B、C三頁,假設程式後面使用的頁數依次是:C、B、A、D、C、B、A、B、C,使用C、B、A的時候不用換頁,當使用D的時候,先看一下後面最遠使用的頁是哪個,然後將它換掉;對於上例,最遠使用的頁是A,於是便將A換出,將D換入。可以看出這種方法是最好的方案,但是它實現不了。因為它需要事先知道程式後續要使用哪些頁,而這點是做不到的。

LRU演算法

唐太宗李世民在《舊唐書·魏徵傳》中說過:“以銅為鏡,可以正衣冠;以古為鏡,可以知興替;以人為鏡,可以明得失。”;其中“以古為鏡,可以知興替”說的就是可以通過歷史來預測未來(當然原句的翻譯是:用歷史當作鏡子,可以知道國家興亡的原因;)。雖然不能知曉未來,但是可以通過前面呼叫的頁的順序來推測未來哪些頁是常用的。理論基礎就是程式的空間區域性性。

LRU演算法就是這樣:選最近最長一段時間沒有使用的頁淘汰(最近最少使用)。

LRU演算法的準確實現:用時間戳

用時間戳來記錄每頁的訪問時間,比如某個程式訪問頁的順序為:A 、B 、C 、A 、B 、D 、A 、D 、B 、C 、B;該程式只有三個頁框。那麼使用時間戳
實現,如下圖:
在這裡插入圖片描述

第一次將A放入頁框中,並記錄當前時間為1;第二次將B放入頁框中,並記錄當前時間為2;第三次將C放入頁框中,並記錄當前時間為3;第四次又是訪問A頁,更新A頁訪問時間,第五次訪問B頁,更新B頁訪問時間;第六次訪問D頁,不存在,那麼就在A、B、C頁中選擇一個最早使用的也就是數字最小的替換,即C頁。這種方式理論上是可行的,但是每次地址訪問都要修改時間戳,需要維護一個全域性時鐘,需要找到最小值……實現代價太大了。

LRU演算法的準確時間:用頁碼棧

還是上面的例子,

在這裡插入圖片描述

每次地址訪問都需要修改棧,實現代價也比較大。其實LRU準確實現用的少,因此維護代價太高了。從上面兩種演算法可以看出,主要是在維護時間戳上面花費的時間比較多,但是能不能將LRU演算法做一個優化呢?或者說近似實現?

clock演算法

LRU的近似實現 - 將時間計數變為是和否

二次機會演算法

具體操作是這樣的,每頁增加一個引用位( R ),每一次訪問該頁時,就將該位置為1。當發生缺頁時用一個指標檢視每一頁的引用位,如果是1則將其置為0,如果是0就直接淘汰。如下圖:

在這裡插入圖片描述

用這種方式實現就不必維護時間戳了,提高了記憶體使用效率。但是這種演算法真的可以使用嗎?在實際中,缺頁的情況肯定不會很多;如果缺頁很多了,說明記憶體太小了或者演算法不行。那當這個演算法缺頁很少的情況會怎麼樣呢?假設初始狀態所有的頁的引用位都是1,這是很有可能的,因為缺頁情況少,程式使用的一直是記憶體裡面存在的頁。那麼當發生缺頁時,指標轉一圈之後將所有的頁的引用位都置為0,沒找到能替換的,繼續轉,這時候發現最開始那個頁引用位為0,將其置換出去,指標後移;然後又一段時間沒有發生缺頁,所有頁的引用位都為1,當發生缺頁之後,又會將這一輪最開始的那一頁置換出去;然後指標後移,一段時間之後發生缺頁,又會將這一輪最開始的那一頁置換出去。wait…,這不是變成FIFO了嗎。

究其原因還是因為指標掃描的時間太長了,也就是記錄了太長的歷史資訊。其中一個解決辦法是再加一個指標用來清除每一頁的引用位,可以放在時鐘中斷裡面,定時清除;這個時間可以事先設定好,也可以在軟體裡面實現。

給程式分配多少個頁框

嗯嗯,現在置換策略有了,但是還有一個問題需要解決:給程式分配多少個頁框。

如果分配的多了,那麼請求調頁導致的記憶體高效利用就沒有了。而且記憶體就那麼大,如果每一個程式分配很多的話,跑的程式數量就少了。如果分配的少,系統內程式增多,每個程式的缺頁率增大,當缺頁率大到一定程度,程式就會總等待調頁完成,導致cpu利用率降低。如下圖

在這裡插入圖片描述

中間那個急劇下降的狀態稱為顛簸。一種可以採用的方法是,先給程式分配一定數量的頁框,如果增加頁框能增加cpu利用率,就慢慢增加,如果導致cpu利用率減少,就降低頁框分配。當然實際情況下每個程式對應的頁框數量肯定是得動態調整的。

參考資料

哈工大李志軍作業系統

相關文章