在看文章前可以先看下這個,

吳海波:專欄的序。

先有個大概的認識會對閱讀有所幫助。

到目前為止,我們已經將每個進程的整個地址空間放入了內存中。 使用基址和界限寄存器,操作系統可以輕鬆重定位進程到物理內存的不同部分。 但是,你可能會注意到這些地址空間的一個奇怪的地方:在堆和棧之間有一大塊「自由」空間。從圖16.1可以看出,雖然在堆和棧中存在沒有使用的內存區域,但當我們將整個地址空間重定位時,它仍佔用物理內存; 因此,使用基址和界限寄存器的簡單方法來對內存虛擬化是浪費。並且如果程序的地址空間大於內存空間,就無法正常運行該程序了。因此這種內存虛擬化的方式是有侷限性的。 因此我們遇到了問題是:

如果將上面提到的地址空間的空閑區域有效進行利用?

針對需要大內存的應用如何進行支持?

1. 分段機制介紹

分段就是對上面問題的解決方法。這種方式很早就被提出,大概是20世紀60年代。這個方法說起來其實很簡單,就是增加基址和界限寄存器的數量,將地址空間分成一段一段的,每一段都是一個連續的地址。這樣代碼,數據,棧就可以在分別的段中存放,操作系統就可以將這些段放入不同的物理內存區域,避免了空閑區域的產生。下面是一個簡單的例子來說明這種方式。

比如圖16.1,如果我們將這個地址空間放入物理內存中,因為有了分段的機制,我們就可以將地址空間中的各個段獨立的放入物理內存中,就像下面圖16.2這樣。

在圖中,只有使用的內存被分配了物理內存,其他的沒有使用的內存可以分配給其他的程序。

在MMU中,提供了我們需要的3個基地址和邊界寄存器,圖16.3給出了對應寄存器的值

下面我們進行一個地址翻譯的聯繫。比如我們想訪問虛擬地址100(對應的是代碼段),那麼基地址寄存器的32k加上這100得到32868,然後再檢查下是否超出了這個段設置的2k的大小,顯然是沒有超出的,那麼最終訪問的物理地址就是32868。

如果試圖訪問一個非法的地址,比如對堆這個段,虛擬地址為7k,那麼硬體會檢測出這個錯誤,os收到硬體通知的這個錯誤後,由相應的異常處理程序進行處理(這個就是Exception中fault的處理,後面會單獨說interrupt和exception的具體產生和處理方式)。

2.如何識別出一個地址對應的是哪個段?

給出一個虛擬的地址,如何識別出這個地址對應的段是哪一個,有如下方法。

1.一種常用的方法就是在地址中使用幾個bit來標明這個地址對應的是哪個段。比如在上面的例子中,我們需要三個段,那麼就將地址中的最高2位來作為段的類型進行標示。

2.另外一種方式就是硬體來判斷對應的內存訪問的地址是從何而來。如果地址是從PC中來,那麼就是訪問代碼段,如果是從棧指令中來就是對應的棧段,其他的都算是堆了。

3.對於棧段的特殊處理

因為棧是從高位到地位進行增長的,所以需要一些特殊的處理。首先就是需要硬體的支持,硬體需要知道這個段的增長方向是從大到小還是從小到大。對於這2種方式的增長,硬體在進行地址翻譯的時候就需要使用不同的方式了,下面通過這個例子來說明下。

如果我們要訪問地址空間中的15kb,那麼對應的物理地址應該是27kb。15kb這個虛擬地址,二進位的表示是這樣的:11 1100 0000 0000 (hex 0x3C00).硬體先檢測最高的2位是11,判斷這個是棧段,然後得到偏移地址是3KB。為了得到正確的地址,我們將3kb減去段可能的最大值4kb,得到-1kb,然後我們再用-1kb加上基地址28kb,得到最終的27kb。

4.段的共享

在一些情況下,一個段中的內容是可以共享使用的,這個會節約內存的使用。為了支持共享,我們需要一些硬體的功能來支持:保護位(protection bits).也就是說對於每個段,增加幾個bit來定義這個段讀,寫,執行的許可權。比如下面圖16.5所示:

有了保護位,前面描述的硬體演算法也必須改變。除了檢查虛擬地址是否在範圍內之內,硬體還必須檢查是否允許特定訪問。如果用戶進程試圖寫入只讀段,或從不可執行段執行,硬體應該引發異常,從而讓OS處理這種違規的操作。

5.這種分段機制還存在什麼問題呢?

現在你應該對分段是如何工作的有一個基本的瞭解。在系統運行時,地址空間的部分被重新定位到物理內存中,因此與我們的簡單方法相比,物理內存節省了很大一部分。具體來說,棧和堆之間的所有未使用的空間不需要在物理內存中分配,從而允許我們在物理內存中配置更多的地址空間。然而,分段提出了一些新的問題。第一個是老問題:操作系統應該在上下文切換上做些什麼?看到這裡你應該有對應的想法了:段寄存器的值必須被保存和恢復。顯然,每個進程都有自己的虛擬地址空間,操作系統必須確保在允許進程再次運行之前正確設置這些寄存器。第二個也是更重要的問題是管理物理內存中的空閑空間。當創建新的地址空間時,操作系統必須能夠在物理內存中為其段找到空間。在此之前,我們假設每個地址空間都是相同大小的,因此物理內存可以被看作是容納進程的一個個的插槽。現在,我們打破這個設想,讓每個進程有許多段,每個段可能有不同的大小。

通常出現的問題是,物理內存很快就會充滿小的空閑內存快,這些塊分散存在,因此很難分配新的段,或者增加現有段的大小。我們將此問題稱為外部碎片;參見圖16.6(左)。假設現在出現了一個進程,希望分配一個20 KB的段。在該示例中,有24 KB的空閑,但不是在一個連續段(相反,在三個非連續塊)。因此,操作系統不能滿足20 KB的請求。解決這個問題的一個辦法是通過重新排列現有的段來壓縮物理內存。例如,操作系統可以停止正在運行的進程,將它們的數據複製到一個連續的內存區域,更改它們的段寄存器值以指向新的物理位置,從而讓內存重新具有很大的連續內存區域。通過這樣做,操作系統使新的分配請求能夠成功。然而,壓縮內存的操作是代價昂貴的,因為複製段是內存密集型的操作,通常會消耗相當多的處理器時間(你要知道內存中的數據是先載入到cpu,然後再由cpu放入新的內存區域中的)。請參見圖16.6(右)壓縮物理內存後的圖表。一種更簡單的方法是使用自由列表(free-list)管理演算法,該演算法試圖使內存儘可能的可以分配大內存段。目前已經有數百種方法,包括經典演算法,如最佳匹配(它保留了一個空閑空間列表,並返回最接近請求者所需分配的空間)、最差匹配,或者更複雜的方案,如buddy演算法。不過不管演算法多麼聰明,外部碎片仍然存在;因此,一個好的演算法只是嘗試最小化它。

第二個也可能更重要的問題是,分段仍然不夠靈活,無法支持完全廣義的稀疏地址空間。例如,如果我們在一個邏輯段中擁有一個大型但使用很少的堆,那麼整個堆必須仍然駐留在內存中才能被訪問。換句話說,如果我們如何使用地址空間的模型不完全匹配底層分段的設計模型,那麼分段就不能很好地工作。因此,我們需要找到一些新的解決辦法。這也是下面章節要討論的重點。


推薦閱讀:
相關文章