前一段時間看了《深入理解Linux核心》對其中的記憶體管理部分花了不少時間,但是還是有很多問題不是很清楚,最近又花了一些時間複習了一下,在這裡記錄下自己的理解和對Linux中記憶體管理的一些看法和認識。
我比較喜歡搞清楚一個技術本身的發展歷程,簡而言之就是這個技術是怎麼發展而來的,在這個技術之前存在哪些技術,這些技術有哪些特點,為什麼會被目前的技術所取代,而目前的技術又解決了之前的技術所存在的哪些問題。弄清楚了這些,我們才能比較清晰的把握某一項技術。有些資料在介紹某個概念的時候直接就介紹這個概念的意義,原理,而對其發展過程和背後的原理絲毫不提,彷彿這個技術從天上掉下來的一樣。介於此,還是以記憶體管理的發展歷程來講述今天的主題。
首先,我必須要闡述一下這篇文章的主題是Linux記憶體管理中的分段和分頁技術。
讓我們來回顧一下歷史,在早期的計算機中,程式是直接執行在實體記憶體上的。換句話說,就是程式在執行的過程中訪問的都是實體地址。如果這個系統只執行一個程式,那麼只要這個程式所需的記憶體不要超過該機器的實體記憶體就不會出現問題,我們也就不需要考慮記憶體管理這個麻煩事了,反正就你一個程式,就這麼點記憶體,吃不吃得飽那是你的事情了。然而現在的系統都是支援多工,多程式的,這樣CPU以及其他硬體的利用率會更高,這個時候我們就要考慮到將系統內有限的實體記憶體如何及時有效的分配給多個程式了,這個事情本身我們就稱之為記憶體管理。
下面舉一個早期的計算機系統中,記憶體分配管理的例子,以便於大家理解。
加入我們有三個程式,程式1,2,3.程式1執行的過程中需要10M記憶體,程式2執行的過程中需要100M記憶體,而程式3執行的過程中需要20M記憶體。如果系統同時需要執行程式A和B,那麼早期的記憶體管理過程大概是這樣的,將實體記憶體的前10M分配給A, 接下來的10M-110M分配給B。這種記憶體管理的方法比較直接,好了,假設我們這個時候想讓程式C也執行,同時假設我們系統的記憶體只有128M,顯然按照這種方法程式C由於記憶體不夠是不能夠執行的。大家知道可以使用虛擬記憶體的技術,記憶體空間不夠的時候可以將程式不需要用到的資料交換到磁碟空間上去,已達到擴充套件記憶體空間的目的。下面我們來看看這種記憶體管理方式存在的幾個比較明顯的問題。就像文章一開始提到的,要很深層次的把握某個技術最好搞清楚其發展歷程。
1.程式地址空間不能隔離
由於程式直接訪問的是實體記憶體,這個時候程式所使用的記憶體空間不是隔離的。舉個例子,就像上面說的A的地址空間是0-10M這個範圍內,但是如果A中有一段程式碼是操作10M-128M這段地址空間內的資料,那麼程式B和程式C就很可能會崩潰(每個程式都可以系統的整個地址空間)。這樣很多惡意程式或者是木馬程式可以輕而易舉的破快其他的程式,系統的安全性也就得不到保障了,這對使用者來說也是不能容忍的。
2.記憶體使用的效率低
如上面提到的,如果我們要像讓程式A、B、C同時執行,那麼唯一的方法就是使用虛擬記憶體技術將一些程式暫時不用的資料寫到磁碟上,在需要的時候再從磁碟讀回記憶體。這裡程式C要執行,將A交換到磁碟上去顯然是不行的,因為程式是需要連續的地址空間的,程式C需要20M的記憶體,而A只有10M的空間,所以需要將程式B交換到磁碟上去,而B足足有100M,可以看到為了執行程式C我們需要將100M的資料從記憶體寫到磁碟,然後在程式B需要執行的時候再從磁碟讀到記憶體,我們知道IO操作比較耗時,所以這個過程效率將會十分低下。
3.程式執行的地址不能確定
程式每次需要執行時,都需要在記憶體中非配一塊足夠大的空閒區域,而問題是這個空閒的位置是不能確定的,這會帶來一些重定位的問題,重定位的問題確定就是程式中引用的變數和函式的地址,如果有不明白童鞋可以去查查編譯願意方面的資料。
記憶體管理無非就是想辦法解決上面三個問題,如何使程式的地址空間隔離,如何提高記憶體的使用效率,如何解決程式執行時的重定位問題?
這裡引用計算機界一句無從考證的名言:“計算機系統裡的任何問題都可以靠引入一箇中間層來解決。”
現在的記憶體管理方法就是在程式和實體記憶體之間引入了虛擬記憶體這個概念。虛擬記憶體位於程式和屋裡記憶體之間,程式只能看見虛擬記憶體,再也不能直接訪問實體記憶體。每個程式都有自己獨立的程式地址空間,這樣就做到了程式隔離。這裡的程式地址空間是指虛擬地址。顧名思義既然是虛擬地址,也就是虛的,不是現實存在的地址空間。
既然我們在程式和實體地址空間之間增加了虛擬地址,那麼就要解決怎麼從虛擬地址對映到實體地址,因為程式最終肯定是執行在實體記憶體中的,主要有分段和分頁兩種技術。
分段(Segmentation):這種方法是人們最開始使用的一種方法,基本思路是將程式所需要的記憶體地址空間大小的虛擬空間對映到某個 實體地址空間。
段對映機制
每個程式都有其獨立的虛擬的獨立的程式地址空間,可以看到程式A和B的虛擬地址空間都是從0x00000000開始的。我們將兩塊大小相同的虛擬地址空間和實際實體地址空間一一對映,即虛擬地址空間中的每個位元組對應於實際地址空間中的每個位元組,這個對映過程由軟體來設定對映的機制,實際的轉換由硬體來完成。
這種分段的機制解決了文章一開始提到的3個問題中的程式地址空間隔離和程式地址重定位的問題。程式A和程式B有自己獨立的虛擬地址空間,而且該虛擬地址空間被對映到了互相不重疊的實體地址空間,如果程式A訪問虛擬地址空間的地址不在0x00000000-0x00A00000這個範圍內,那麼核心就會拒絕這個請求,所以它解決了隔離地址空間的問題。我們應用程式A只需要關心其虛擬地址空間0x00000000-0x00A00000,而其被對映到哪個實體地址我們無需關心,所以程式永遠按照這個虛擬地址空間來放置變數,程式碼,不需要重新定位。
無論如何分段機制解決了上面兩個問題,是一個很大的進步,但是對於記憶體效率問題仍然無能為力。因為這種記憶體對映機制仍然是以程式為單位,當記憶體不足時仍然需要將整個程式交換到磁碟,這樣記憶體使用的效率仍然很低。那麼,怎麼才算高效率的記憶體使用呢。事實上,根據程式的區域性性執行原理,一個程式在執行的過程當中,在某個時間段內,只有一小部分資料會被經常用到。所以我們需要更加小粒度的記憶體分割和對映方法,此時是否會想到Linux中的Buddy演算法和slab記憶體分配機制呢,哈哈。另一種將虛擬地址轉換為實體地址的方法分頁機制應運而生了。
分頁機制:
分頁機制就是把記憶體地址空間分為若干個很小的固定大小的頁,每一頁的大小由記憶體決定,就像Linux中ext檔案系統將磁碟分成若干個Block一樣,這樣做是分別是為了提高記憶體和磁碟的利用率。試想以下,如果將磁碟空間分成N等份,每一份的大小(一個Block)是1M,如果我想儲存在磁碟上的檔案是1K位元組,那麼其餘的999位元組是不是浪費了。所以需要更加細粒度的磁碟分割方式,我們可以將Block設定得小一點,這當然是根據所存放檔案的大小來綜合考慮的,好像有點跑題了,我只是想說,記憶體中的分頁機制跟ext檔案系統中的磁碟分割機制非常相似。
Linux中一般頁的大小是4KB,我們把程式的地址空間按頁分割,把常用的資料和內碼表裝載到記憶體中,不常用的程式碼和資料儲存在磁碟中,我們還是以一個例子來說明,如下圖:
程式虛擬地址空間、實體地址空間和磁碟之間的頁對映關係
我們可以看到程式1和程式2的虛擬地址空間都被對映到了不連續的實體地址空間內(這個意義很大,如果有一天我們的連續實體地址空間不夠,但是不連續的地址空間很多,如果沒有這種技術,我們的程式就沒有辦法執行),甚至他們共用了一部分實體地址空間,這就是共享記憶體。
程式1的虛擬頁VP2和VP3被交換到了磁碟中,在程式需要這兩頁的時候,Linux核心會產生一個缺頁異常,然後異常管理程式會將其讀到記憶體中。
這就是分頁機制的原理,當然Linux中的分頁機制的實現還是比較複雜的,透過了也全域性目錄,也上級目錄,頁中級目錄,頁表等幾級的分頁機制來實現的,但是基本的工作原理是不會變的。
分頁機制的實現需要硬體的實現,這個硬體名字叫做MMU(Memory Management Unit),他就是專門負責從虛擬地址到實體地址轉換的,也就是從虛擬頁找到物理頁。