作業系統(八) -- 記憶體的分段與分頁

williamgavin發表於2018-10-20

前言

cpu的使用基本上告一段落,接下來是記憶體部分。

正文

記憶體如何使用:

記憶體使用就是放在記憶體中的程式能夠按照正確的邏輯順序執行

首先讓程式進入記憶體:

問題引入

假設一段c程式碼

int main (int argc, char * argv[])
{
	
	………………
}

編譯之後形成的彙編程式碼如下:

_entry:
	call _main
	call_exit
_main:
……………………
ret

entry是入口地址,如果_main相對於_entry的偏移地址是40

_entry:		// 入口地址
	call 40
	call _exit
_main:		// 偏移為40
………………

現在如果這段程式要執行,那麼只要將PC指向 call 40 這條指令所在的地址就好了,執行完call 40之後,會自動跳到地址為40處執行。但是現在有個問題,_main所在的位置一定是實體地址為40的位置嗎?換言之,40表示的是實體地址嗎?

肯定不是,在作業系統啟動的時候就說過,作業系統載入到記憶體之後會放在從實體地址為0開始的一段記憶體裡面,因此call 40這條指令的40絕對不能表示實體地址40

初始邏輯地址與實體地址

call 40這個40指的是相對於_entry的偏移量,程式裡面的地址是相對地址(邏輯地址),而程式真正執行時的地址是絕對地址(實體地址),即程式執行時這個40肯定是要改的,根據邏輯地址得到實體地址就是地址的重定位。比如_entry這條指令的地址如果存放在實體地址為1000處,那麼_main的
地址就應該是1040,所以call 40就要變成call 1040.

執行時進行重定位。

在什麼時候進行地址的重定位呢?編譯時?載入時?還是執行時?

首先看下如果是編譯時就進行地址重定位,程式編譯之後的程式碼如果不執行的話是放在磁碟裡面的,如果編譯的時候從地址1000開始處有一段空閒記憶體足夠該程式使用,這時候進行地址重定位,將基地址設定為1000。也就是call 40就變成1040。但是程式執行的時候並不能保證這塊記憶體仍然還是空閒的,因此編譯時進行重定位不行。

如果是載入時重定位呢?當載入記憶體的時候再進行地址重定位;看起來好像沒問題;但是CPU是多程式執行的,而且記憶體相對於磁碟來說容量是比較小的,假設程式一存放在記憶體以1000開始的位置,可能某一時刻程式一阻塞了,而且時間還不短,這時候如果再將其放在記憶體裡面肯定是不合理的,因此會將程式一換出到磁碟,1000開始的這個位置變成了空閒區,可能被其他程式佔用了,下次程式一換入的時候可能就是在2000這個位置了,但是如果是載入時重定位就意味著在解釋地址的時候還是以1000為基址的,所以也不行。

執行時重定位指的是在執行call 40這條指令的時候才將40這個邏輯地址轉化成
實際的實體地址,如果程式存放在1000開始的位置,那麼就是call 1040,如果是存放在2000的位置,那麼就是call 2040;沒毛病。

還有一個問題,從上面的分析也能看出,實體地址=基址+邏輯地址。那麼這個基址是放在哪個地方呢?每個程式都需要有自己的基址,每個程式…等等,每個程式都有一個專門用來存放該程式資訊的資料結構,即PCB;因此可以將該程式的基址放在PCB中。程式切換根據PCB切換一起切換這個基地址。

記憶體的分段機制

前面說得都是一次將整個程式放入到某一塊空閒記憶體裡面。但是事實上是這樣嗎?不是的,因為記憶體是分段,為什麼記憶體會分段?因為程式是分段的。

程式設計師眼中的程式:
在這裡插入圖片描述

在程式設計師眼中的程式是分為很多段的,每一段都有不同的特點。適用於不同的領域。每一段都是從該段的地址0開始的。就是說主程式存放的地方地址應該是從零開始的,變數存放的地方地址也應該是從零開始的,其他區域也是如此。使用者程式裡面每個區域都有其自己的特點,比如主程式這部分應該是隻讀的,變數所在的區域是可寫的,函式庫應該是可以可以連結也可以不連結的,棧應該只能單向增加。如果是將整個程式都放在一塊的話這些要求肯定不能保證。因此程式應該是要分段儲存的,並且這些段都有自己的特點。

既然是分段的,那麼是怎麼定義地址的呢?還是基址+偏移。只不過這裡的基址不再是這個程式的起始位置了,而是這一段程式的起始地址。這個基址放在段表裡面

在這裡插入圖片描述
CPU每執行一條牽涉到地址的指令都會查一下PCB裡面這個程式段表,從而確定實體地址。這個表其實就是LDT表,有一個專門存放該表地址的暫存器LDTR暫存器。到目前為止記憶體已經可以使用起來了。因為地址已經設定好了。


程式執行應該首先將程式從磁碟上面讀到記憶體的空閒區域,這就引出了一個問題:如何在記憶體裡面找到空閒分割槽。

如何在記憶體裡面找到空閒分割槽。

首先考慮的問題應該是如何分割記憶體,前面說的將記憶體分段以使用者的角度看的,現在來單純的考慮記憶體該如何分割槽。

固定分割槽

第一種方式固定分割槽,即作業系統初始化的時候將記憶體等分為n個分割槽,大小一樣。但是程式執行的時候記憶體的需求有大有小,如果採用這種方式勢必會造成很大的浪費。

可變分割槽

第二種方式可變分割槽,可變分割槽的基本思想是建立已分配分割槽表和空閒分割槽表,已分配分割槽表中記錄了已經使用了的記憶體有哪些,註明了這一段記憶體是哪個程式使用了,起始地址和長度是多少。空閒分割槽表記錄了記憶體中的空閒區域,包括起始地址和長度。這時候如果有段記憶體請求,根據請求的記憶體大小以及空閒分割槽表上面空閒分割槽的大小來給這個請求分配記憶體,同時更新這兩張表;如果有程式執行完了也同樣更新這兩張表。這樣做的好處是:可以給需要大記憶體的程式分配大塊記憶體,給需要小記憶體的程式分配小記憶體,提高記憶體利用率。

可變分割槽的三種適配方式

可變分割槽的方式還有一個需要考慮的問題,比如有一個請求需要40K記憶體,空閒分割槽表裡面有很多個大於40K的記憶體區域,應該選擇哪一個分配呢?

首先適配,顧名思義,就是將第一個符合該請求的記憶體分配出去。這樣的好處是:快。時間複雜度是O(1)

最佳適配,把所有的空閒記憶體塊都看一遍,將最接近40K並且大於40K的記憶體分配給它。這樣的好處是可以可以提高記憶體的使用率。

最差適配,把所有的空閒記憶體塊都看一遍,將最大塊的記憶體分配給它,並且該記憶體塊一定大於40K。這樣的好處是剩下的記憶體塊都比較均勻。

三種適配方式各有其優劣;選擇哪種適配方式要根據實際情況來。

可變分割槽造成的問題

比如有一個160K的記憶體請求,但是空閒分割槽表裡面只有一個150K的和一個50K的,都不夠,怎麼辦。這其實就是記憶體碎片。一種方式是記憶體緊縮,即將150K的記憶體大小和50K的想辦法移到一起。但是這種方式比較耗費時間,並且這段時間電腦是不能工作的。那有沒有方法可以消除掉記憶體碎片呢?答案是將記憶體分頁

將頁作為記憶體分配的最小單元,假設一頁為4K,如果一個記憶體請求是13K,那麼就給它分配4頁。這樣一個程式最多也只會浪費不到4K的記憶體區域,這種浪費是很小的,因此也不需要進行記憶體緊縮了。那這種方式會產生記憶體碎片嗎?其實也有會一些記憶體沒有使用到,但是注意這種思想,頁是一個單位,每次分配的記憶體都是整數個頁,也就是將這些分配出去的頁都看成是已經使用的了,所以就沒有記憶體碎片。因此從記憶體角度來說這種方式是比較好的,也就是實體記憶體想要分頁,但是使用者程式希望是分段。那這個段和頁是如何結合的呢?後面會講。

如何根據邏輯地址找到實體地址

記憶體分段的時候有一個段表,分頁的時候自然也要有一個頁表。有一個專門的暫存器儲存頁表的地址。注意頁在記憶體裡面的排布順序並不是按照地址的順序遞增的,也就是說頁0不一定是放在地址零處;這裡引入頁框,頁框是按照記憶體順序排列的,並且頁框的大小和頁是相同的。頁表如下圖:
在這裡插入圖片描述
看一個實際的例子吧。

mov [0x2240],%eax

2240這個地址表示的實際記憶體地址是多少呢?首先看它是那一頁的,每一頁的大小為4k,0x2240除以4K得到頁號,除以4K也就是右移12位。得到2.即第二頁;根據這個頁號找到具體的頁框號,在上圖中為3,那麼具體的地址就是3*4K+240=3240,即實體地址為3240.

參考資料

哈工大李志軍作業系統

相關文章