1. 程式人生 > >linux記憶體管理解析----linux物理,線性記憶體佈局及頁表的初始化

linux記憶體管理解析----linux物理,線性記憶體佈局及頁表的初始化

早就想搞一下記憶體問題了!這次正趁著搞bigmemory核心,可以寫一篇文章了。本文旨在記錄,不包含細節,細節的話,google,百度均可,很多人已經寫了不少了。我只是按照自己的理解記錄一下記憶體的點點滴滴而已,沒有一家之言,不討論,不較真。

1.最簡單的記憶體使用

最簡單的模型是馮.諾依曼提出的原始模型,簡單的把資料和指令存放在記憶體中,然後機器從記憶體中取出指令和資料進行計算,如下圖所示:

當時的機器是為了執行一個特定的任務,但是這種儲存執行模型作為的一個最簡單的核心,為後代的逐步複雜化奠定了基礎。我的觀點還是,一個概念或者其它什麼東西,之所以複雜是因為它經得起復雜。羅馬帝國始終脫離不了城邦格局,它經不起復雜,它崩潰了!

2.分時系統記憶體模型

2.1.計劃

一臺機器執行一個任務,太浪費資源了,略過中間的掙扎,直接到了分時系統時代,一臺機器可以執行多個任務了,如果切換機制足夠好,這些任務可以滿足低延遲需求。馮諾依曼機器的核心是處理器和記憶體,處理器沿時間軸推進,記憶體則空間平面上展開,分時系統在時間軸上分割了多個任務,它需要在空間平面上同樣分割多個任務,於是記憶體變成了一種共享的資源,如何在多個任務之間分配這個單一的共享的記憶體資源成了後來技術發展的重軸戲。

2.2.段式記憶體模型

段式記憶體模型將一個程式劃分為不同的段,不同任務的不同段處在記憶體的不同段當中,如下圖所示:

但是,這種模型有兩個顯而易見的缺點,第一,很難滿足一個大記憶體需求的任務,第二,一分就是一個段,段必須處在一個連續的空間。

2.3.虛擬記憶體

一個任務所需要的記憶體大小以及位置不應該依賴其它任務的記憶體的大小和位置,並且記憶體的位置也不應該是永久性的,任務使用記憶體就應該和人們使用公共廁所一樣。程式任務只管自己的計算邏輯,用到記憶體的時候,不必自己操心,應該有一個服務機構為其現場分配記憶體,分配多少算好呢,答案就是就可能少,按照基本單位分配,也就是說只分配程式現在用的那個記憶體,即便說馬上就要用另一塊記憶體,那也要等到時候再說,這樣就做到了公平和高效!既滿足了儘可能多的程式的記憶體需求,又不會浪費任何不會用到的記憶體。
       記憶體頁面的概念被提出後,頁就成了分配記憶體的最小單位,而MMU則成了為程式分配記憶體的服務管理機構,有了這個新機構,應用程式再也不用考慮實體記憶體的位置的大小以及偏移問題了。
       虛擬記憶體的提出是革命性的,在以前,程式不得不維護自己段暫存器,以明確自己所需記憶體的位置,只要是有一個地址,就可以根據段暫存器知道它位於記憶體中的什麼地方,也就是說,那個時候,程式是直接使用實體記憶體的。虛擬記憶體出現後,MMU接管了記憶體管理的一切,應用程式不必關心記憶體的位置和大小了。如果是32位系統,那麼程式被承諾可以使用高達4G的記憶體,如果是64位系統,...至於自己使用的記憶體在什麼位置,則不必關心,可用的4G記憶體只是許諾,等到需要的時候MMU自然會給你,如果沒有空閒記憶體,自然會給你個說法。MMU作為一個仲裁和管理機構,前提是大家必須信任它!
       如果說初期的直接獨佔使用實體記憶體是王政時代, 段式管理是貴族寡頭時代的話,虛擬記憶體管理則真正到了民主時代,各項機構有條不紊執行。

2.4.頁式記憶體模型

頁式模型顧名思義就是以頁面為基礎進行記憶體管理,注意,最終的記憶體頁面並不直接和程式打交道,它通過MMU和程式打交道。由於有了MMU這個中間層,它負責將一個程式的虛擬記憶體地址對映到實際的實體地址,怎麼做到的呢?當然是通過一張表,即頁表來查詢的。

由於採用了MMU這個中間層,實體記憶體不再和程式直接打交道,則實體記憶體的形式就變得不再重要,它可以是記憶體條,也可以是磁碟,甚至可以是裝置,只要MMU能給出合理的解釋,並且按照應用程式訪問記憶體的規則來訪問這些實體並能給出正確的結果即可。這就使得檔案對映,裝置對映成了可能。如下圖所示:

       雖然說“從虛擬記憶體對映到實體記憶體通過查表可以實現”,但是具體的查表過程卻非常複雜,並不是簡單的一對一的對映這麼實現的。實際的實現是通過一個多極頁表的方式實現的。所謂的多級頁表是將虛擬地址分為不同的部分,每一部分代表不同的索引。這樣就可以按照記憶體的範圍進行區域劃分,更好的進行頁表的管理。具體的方法和圖示無須google,百度即可!

附:一些細節-為何採用多級頁表

在實際的實現中,為何要使用多級頁表而不是單級頁表呢?這是從管理成本來考慮的。由於是一個表,那麼它便有連續記憶體儲存的需求,這樣才好根據索引來快速定位。如果是單層頁表,那麼即使一個頁面被分配,也需要建立整個頁表,32位的情況下以4K頁面為例,需要20位要定址頁面基地址,20位的話單級頁表需要一下子建立4M大小的頁表。
       使用多級頁表並不是為了減少記憶體使用,說實話,如果把所有的4G對映都建立頁表項的話,採用兩極頁表還會浪費頁目錄表佔用的4K空間,然而並不能如此考慮問題,記憶體使用分佈是不遵循冪率的,因此你不必考慮黑天鵝事件。大部分情況下,不會建立太多的頁表,即使建立1000個頁表,它也會多數承載於連續的頁目錄項中,很多的頁表是不需要分配記憶體的。主旨就是,將管理結構分級往前推,往前推,往前推!
       本文最後會給出一個程式,讓你眼見為實地明白頁表到底佔據多少空間以及記憶體的分佈如何影響頁表佔據記憶體空間的大小。

2.5.換入換出機制

有點懵了,怎麼現在才開始說換入換出,是不是順序弄亂了,不是說很早的UNIX時代就有換入換出了麼?如今Linux還保留著swap程序(其實是核心執行緒,因為總有人較真,說什麼核心執行緒不能叫做程序,看書看多了)這個名稱。非也,不是弄錯了,而是我想基於交換機制來談一下虛擬記憶體的意義,並不是講換入換出機制本身。

2.5.1.整體換入換出

這是最實際不過的了,分時系統將一個程序拉到前臺來執行的時候,將該程序的映像從磁碟換到記憶體,同時將正在執行的程序換出到磁碟。雖然簡單,但是卻沒有後續的可擴充套件性。這其實是基於最初的記憶體模型修正的,而絲毫沒有用到虛擬記憶體的優勢。虛擬記憶體不關心程序,不關心記憶體頁面的位置,切斷了實體記憶體和程序的關係,只關注頁面本身,頁面的內容可以來自計算,來自檔案,來自裝置,...
       事實上,整體的換入換出模型更加適合虛擬化,所謂的虛擬化指的是全部的,包括CPU在內的虛擬化。它不適合虛擬記憶體,既然CPU都是分時處理不同程序的,記憶體為何就不能分頁面對映給不同的程序呢?

2.5.2.頁面換入換出

由整體換入換出的弊端引起的直接結果就是頁面的換入換出。一個頁面就是一個頁面,它不和程序進行關聯,只和頁表項進行關聯,這是虛擬記憶體管理高效性和公平性之根本。單獨頁面的管理以及基於頁面的換入換出機制,虛擬記憶體之根本!

3.實體記憶體的角色

在使用虛擬記憶體之後,實體記憶體的角色已經不再像早期那樣重要,它退化成了一個資源的角色,作為一種資源,原則上它可以是無限大的,而且越大越好,但是受制於以下的因素:
a.管理成本:是資源就要有效管理,而管理本身也是消耗資源的,它在各個系統上都有一個上限。
b.體系結構的相容性考慮:如果說從一張白紙重新作畫,那再簡單不過了,然而現實並非如此,匯流排寬度為了相容性並不能隨意擴充套件。
c.實現成本:計算機上的任何概念都是一個有限集,更加明確的原則就是,它不要求100%的好,而是要求90%可用即可。
d.電梯效應:超高層的摩天大樓不可能建造,並不是因為底層承載不了上層的持續壓力,而是如果建成了高層大廈,建得越高,電梯佔據的空間就越大,達到閥值後,電梯的空間將超越使用空間。這是管理成本的另一層含義,只是更加嚴重些!
即使不能使實體記憶體無限大,也可以讓它更大,PAE就是這個想法的產物。

4.對映到實體記憶體

實體記憶體並非一定要和虛擬記憶體的大小一致,如果是這樣的話,虛擬記憶體的意義是不明顯的。以32位系統為例,N個程序均被許諾有4G記憶體,然而它們共享4G實體記憶體,誰也不能同時用盡所有記憶體,然而如果真的有這樣需求的程序怎麼辦?那隻能頻繁的換入換出了,雖然也是可以實現,但是更好的做法就是安裝N*4G的記憶體,可是這樣的話好像又退回到了段式管理,只是記憶體的實際位置不再確定,只是將基於段的管理改成了粒度更細的頁式管理而已。由於N的不確定性以及記憶體使用的不確定性,沒有必要安裝那麼大的記憶體,最終的方案就是安裝稍微大一些的記憶體,比如16G,32G,64G的記憶體,機器就足以飛起來了!
       到此為止,可能你還是不明白32位的系統如何去識別4G以上的記憶體。注意,MMU使用頁表來定位頁面的位置,只要頁表項能填入一個大於32位的數字,就能定址到4G以上的記憶體--因為32位可以定址4G(why?...),加上硬體地址匯流排寬度能超越32位,那就能定位到大於4G的記憶體,定位到這個頁面後,將其映射回32位的某個地址即可。如下圖所示:

記住,實體記憶體僅僅就是一個資源的角色,在不考慮相容性以及管理成本的情況下,當然越大越好,並非非要和虛擬地址空間一致。在32位系統中,程序的虛擬地址空間永遠都是32位也就是4G的,但是這4G空間的地址卻是可以對映到任意的實體地址空間,一切盡在MMU,說白了就是,第一,頁表項的對映指示到了任意的物理頁面,第二就是地址匯流排的寬度允許定址到那個位置(否則,雖然從程式上講不會出錯,但是在地址總線上發射地址的時候會發生迴繞!)。程式設計師可以照著書上的例子寫出程式碼,但是沒有什麼書教你在哪些機器上這些程式碼可以得到你預期的結果!!

5.Linux上的實現與PAE/PSE相關

上面說的加大實體記憶體供應的說法,其實有一種實現那就是PAE。PAE是什麼,google吧,如果怕google動不動就RESET,那麼百度也能得到結果!PAE允許你定址36位的實體地址空間!也就是說允許你安裝64G的記憶體。按照實體記憶體只是資源池的概念,所有的32位程序共享所有安裝的實體記憶體,每一個程序定址32位虛擬地址,通過MMU實際可以訪問36位的實體地址。
       是時候說一下Linux了,對於實踐者和懷疑論者以及書生乃至抑鬱症患者抑或精神病而言,沒有任何高談闊論可以比得上一個實際的例子了。

5.1.Linux的地址對映方式

Linux採用了一種極其簡單的地址對映方式,那就是將核心空間的程式碼以及資料和實際的使用者程序隔離開來,怎麼個簡單法呢?很簡單,那就是將程序的地址空間劃分為使用者態的3G和核心態的1G,所謂的使用者態就是非特權態,對於那些愛看書的優秀學生而言,他們熟悉的語言是第3特權護環,不管怎麼說,反正就是程序可見的資料和程式碼的地址空間!使用者態的地址空間對映,核心不過問,而核心態的對映,所有的使用者態共享,作為一個管理機構,它是唯一的,如下圖所示:

使用者態可以有自己的對映,核心態的對映全部交給了核心本身!核心簡化了,它可以用一種更加簡單且高效的方式實現管理,那就是一一線性對映,也就是將一個連續的核心地址空間,對映到一塊連續的實體地址空間,雖然最終的訪存還是需要MMU,但是起碼不需要做複雜的管理工作了。對映到哪塊連續的實體地址空間好呢?當然是最初的空間好,因為核心的對映位置不能依賴實體地址空間的大小。
       以上就是一一線性對映的由來!但是為了滿足動態的核心態服務的記憶體需求,比如動態插拔的核心模組,比如使用者系統呼叫的臨時需求,核心的虛擬地址空間還要留下一部分用來對映這些動態的資料。最終核心態的虛擬地址空間的佈局成了如下佈局:

這樣的佈局本身沒有什麼問題,特別是如果你理解Windows的自對映以及核心分頁機制之後,你就會發現Linux的方式是多麼的原生態,多麼的環保。然而這種方式有一個疑問-現如今還不能成為問題:
為了保持一一對映的關係,所有的一一對映的記憶體必須獨佔且常駐記憶體,和最原始的馮諾依曼機器實現那種方式一樣,因此,Linux的一一對映方式真正迴歸了原生態,只是中間有一個MMU例行公事而已!
       現在考慮PAE的模式!我討厭《尼羅河上的慘案》中那個穿西服的傢伙那種方式!如果啟動了PAE,意味著系統中存在大量的記憶體頁面,為了管理這些頁面,Linux核心必須為這些頁面建立結構體,即struct page。Linux系統是按照夥伴系統分配page的,夥伴系統要求事先必須存在page的索引,因此必須要考慮page結構體們佔據的記憶體空間。作為基礎管理資料結構,這些page結構都處在核心的一一對映地址空間,然而一一對映的地址空間大小是有限制的,即896M!按照一個page結構體32位元組大小來計算的話,你算一下能允許多少page被索引,記住,896M不能全都用於page結構,記憶體管理只是Linux核心的一部分而已,另外還有大頭戲,程序管理!結果就是在現行的Linux記憶體管理模式下,只能管理有限的page,因此你並不能安裝64G的記憶體在Linux系統上。
       但是,作為一個通用且前衛的系統,關鍵是Linus動不動就動粗口的情形下,Linux有自己的解決方案,其中不外乎以下兩點:
a.使用大頁面:一般而言,一個頁面4K,這樣為了索引大記憶體就需要大量的頁面,但是如果一個頁面4M或者2M的話,索引大量記憶體就不需要大量的頁面了,頁面數量減少了,頁面管理結構所佔據的記憶體空間也就減少了!
b.使用獨立的4G/4G模式:雖然大頁面可以緩解管理結構佔據記憶體太大的問題,但是並不能解決!Linux支援一種4G/4G模式,即不再將所有程序的最上面1G的核心空間共享,而是每個程序的使用者態獨佔4G虛擬地址空間,切換到核心態時,同時切換到另一個4G地址空間,即獨立的4G的核心地址空間!核心態不再借用使用者程序地址空間,而是獨立出來一個4G空間來定址,在X86上,意味著不管是系統呼叫,不管是中斷還是異常,陷入核心時,都要切換CR3暫存器,這意味著你要付出一些代價!切換CR3的代價是昂貴的,它不光是save/restore的代價,更是取消了cache加速的代價!

5.2.Linux核心的管理成本

說白了,Linux核心的管理成本太昂貴,很多的管理資料結構都要佔據核心的一一線性對映的896M的空間!雖然程序結構task_struct結構體不大,但是896M除以sizeof(struct task_struct)的話,也不是一個很大的數,再加上CR3指向的頁目錄頁表等,896M真的容不下什麼太多的內容!最根本的限制那就是這種機制限制了系統同時執行程序的數量!但是,Linux核心的這種原始記憶體對映的優點也顯而易見,那就是社群大牛們總是可以設計出一些精巧的資料結構,往往在惡劣環境下寫出的程式碼,一定是高效的程式碼,這同時也是微軟的信條!

5.3.看一段程式碼展示管理成本

是時候展示一個簡單的程式碼了。該程式碼讓你看一下管理成本,雖然很簡單,但是可以讓你看到一個程序本身雖然不佔據什麼記憶體空間,但是光其頁表就佔據大量的記憶體。在展示程式碼之前,先看一個Linux核心的編譯巨集HIGHPTE,該巨集決定你能不能將頁表這種吃記憶體的管理結構分配在高階記憶體,即大於896M的實體記憶體,也就是說如果你啟用了該巨集,就不必在一一線性對映區域分配頁表!我的系統啟用了該巨集,意味著頁表都儘量分配在高階記憶體。以下是程式碼:
  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <sys/mman.h>  
  4. int main(int argc, char **argv)  
  5. {  
  6.     int i = 0, total = 0;  
  7.     for (i = 0; i < 1024; i ++) {  
  8.         int j = 0;  
  9.         for (j = 0; j < 1024; j++) {  
  10.             //保證每一個頁表起碼有一個頁面被分配,這樣是為了填充滿整個頁目錄  
  11.             //所以要用FIX引數,使用LOCK引數目的在於使其頁表項中有內容,否則就是缺頁中斷按需分配了  
  12.             char *p = (char *)mmap(i*1024*4096+j*4096, 16, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS|MAP_FIXED|MAP_LOCKED, -1, 0);  
  13.             //註釋掉的是:隨機分配頁面,不必填充,每一個頁目錄項  
  14.             //char *p = (char *)mmap(NULL, 16, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS|MAP_LOCKED, -1, 0);  
  15.             if((int)p != -1) {  
  16.                 total ++;  
  17.                 *p = 13;  
  18.                 printf("addr:%p  %d  %d\n", p, i, j);  
  19.                 break;  
  20.             }  
  21.         }  
  22.     }  
  23.     printf("total:%d\n", total);  
  24.     sleep(10000);  
  25. }  

執行前,檢視/proc/meminfo後,由於頁表分配在高階記憶體,即896上的記憶體(我的系統2000M記憶體),其內容為:
HighTotal:       1628104 kB
HighFree:        1261560 kB

執行後,total數目為768,符合預期,因為上1G的記憶體屬於核心空間,不能mmap。檢視/proc/meminfo後,HIGH記憶體減小,減小多少自己算
HighTotal:       1628104 kB
HighFree:        1255360 kB

如果不用FIX引數,那麼頁表項同樣也是建立那麼多,在我測試下來,還多了很多,1024個頁表項全部建立成功,但是,HIGH記憶體佔據反而減少了:
HighTotal:       1628104 kB
HighFree:        1257344 kB

這說明記憶體分配的佈局,也就是頁表有與否,會影響管理成本!什麼是High記憶體,是大於896M的所有實體記憶體!可用通過/proc/meminfo看出來!

6.IT技術並不絕對

到底安裝多少實體記憶體算多,可以算出來嗎?IT技術算精確技術嗎?我不覺得喜歡較真的人能徹底理解TCP。衛星技術要比IT技術高深TMD的多了,怎麼沒有人能預測出歐洲衛星殘骸墜落地球的具體地點,追究TCP重傳具體時間具體演算法的人可能要徹夜計算衛星殘骸墜落地點了,由於精神高度緊張,猝死的可能性比猜中50%的可能性更高!
       實際上,很多技術都是基於概率的,都是追求90%的可用而不是100%的完美!為何大家不必為衛星殘骸墜落地球而擔心,因為地球上70%都是海洋,陸地上人類聚集的地點不足1%,所以砸中人的概率字計算吧。如果我被砸中了,算是我對較真的神詛咒的一種報應吧,但是並不絕對!

7.64位,TMD64位!

曾經,大家不約而同地使用64位系統,實際上沒有誰的系統可以用到大記憶體,就算吃記憶體的遊戲,32位也已經足夠,關鍵的是能申請到實體記憶體。只要實體記憶體大即可,虛擬記憶體有誰會用到那麼大呢?你更多的受益於多核而不是64位。我們對時間的感覺要比對空間的感覺敏感得多。
       按照區域性性原理,一個CPU在同一時刻只能處理有限區域的記憶體資料!時間比空間更重要,強勁的CPU要比64位的虛擬地址空間更加有用,虛擬地址空間並不是實際落實的物理地記憶體空間,效率和速度體現在落實的實體記憶體上!即使是實體記憶體的因素,安裝比較大的實體記憶體更多的是在於減少換入換出開銷,而不是為了滿足同時訪問大量記憶體的需求。即使有同時需要大量記憶體的程式,也可以通過併發處理,通過多核來將其平坦化!
       我並不詆譭64位,只是覺得按照資源池的概念,你只需要在乎實體記憶體,而無需在乎虛擬記憶體!別提資料庫,我討厭資料庫,因為它總是為自己佔據大量磁碟需要很高的CPU處理能力而狂呼資源不夠,但是實際上早期的ER模型並不適合現在的大資料處理!是資料庫本身的問題,而不是處理資源不夠!
       與其64位,不如來個8核,已經有了8核,32位夠了!