Linux的內存管理介紹

本篇文章給大家帶來的內容是介紹linux的內存管理,讓大家了解linux內存管理的相關知識。有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。

在linux下,使用top,vmstat,free等命令查看系統或者進程的內存使用情況時,經常看到buff/cache memeory,swap,avail Mem等,他們都代表什么意思呢?這篇文章將來聊一聊Linux下的內存管理并解答這個問題。

討論Linux下的內存管理其實就是討論Linux下虛擬內存的實現方式,本人不是內核專家,所以這篇文章只會介紹一些概念性的東西,不會深入實現細節,有些地方描述的有可能不精確。

在早些時候,物理內存比較有限,人們希望程序可以使用的內存空間能超過實際物理內存,于是出現了虛擬內存的概念,不過隨著時間的推移,虛擬內存的意義已經遠遠的超過了最初的想法。

一、虛擬內存

虛擬內存是Linux管理內存的一種技術。它使得每個應用程序都認為自己擁有獨立且連續的可用的內存空間(一段連續完整的地址空間),而實際上,它通常是被映射到多個物理內存段,還有部分暫時存儲在外部磁盤存儲器上,在需要時再加載到內存中來。

每個進程所能使用的虛擬地址大小和CPU位數有關,在32位的系統上,虛擬地址空間大小是4G,在64位系統上,是2^64=?(算不過來了)。而實際的物理內存可能遠遠小于虛擬地址空間的大小。

虛擬地址和進程息息相關,不同進程里的同一個虛擬地址指向的物理地址不一定一樣,所以離開進程談虛擬地址沒有任何意義。

注意:網上很多文章將虛擬內存等同于交換空間,其實描述不夠嚴謹,交換空間只是虛擬內存這張大藍圖中的一部分。

二、虛擬內存和物理內存的關系

下面這張表很直觀的表述了它們之間的關系

  進程X                                                                      進程Y +-------+                                                                  +-------+ | VPFN7 |--+                                                               | VPFN7 | +-------+  |       進程X的                                 進程Y的           +-------+ | VPFN6 |  |      Page table                              Page Table     +-| VPFN6 | +-------+  |      +------+                                +------+       | +-------+ | VPFN5 |  +----->| .... |---+                    +-------| .... |<---+  | | VPFN5 | +-------+         +------+   |        +------+    |       +------+    |  | +-------+ | VPFN4 |    +--->| .... |---+-+      | PFN4 |    |       | .... |    |  | | VPFN4 | +-------+    |    +------+   | |      +------+    |       +------+    |  | +-------+ | VPFN3 |--+ |    | .... |   | | +--->| PFN3 |<---+  +----| .... |<---+--+ | VPFN3 | +-------+  | |    +------+   | | |    +------+       |    +------+    |    +-------+ | VPFN2 |  +-+--->| .... |---+-+-+    | PFN2 |<------+    | .... |    |    | VPFN2 | +-------+    |    +------+   | |      +------+            +------+    |    +-------+ | VPFN1 |    |               | +----->| FPN1 |                        +----| VPFN1 | +-------+    |               |        +------+                             +-------+ | VPFN0 |----+               +------->| PFN0 |                             | VPFN0 | +-------+                             +------+                             +-------+  虛擬內存                               物理內存                               虛擬內存   PFN(the page frame number): 頁編號

當進程執行一個程序時,需要先從先內存中讀取該進程的指令,然后執行,獲取指令時用到的就是虛擬地址,這個地址是程序鏈接時確定的(內核加載并初始化進程時會調整動態庫的地址范圍),為了獲取到實際的數據,CPU需要將虛擬地址轉換成物理地址,CPU轉換地址時需要用到進程的page table,而page table里面的數據由操作系統維護。

注意:Linux內核代碼訪問內存時用的都是實際的物理地址,所以不存在虛擬地址到物理地址的轉換,只有應用層程序才需要。

為了轉換方便,Linux將虛擬內存和物理內存都拆分為固定大小的頁,x86的系統一般內存頁大小是4K,每個頁都會分配一個唯一的編號,這就是頁編號(PFN).

從上面的圖中可以看出,虛擬內存和物理內存的page之間通過page table進行映射。進程X和Y的虛擬內存是相互獨立的,且page table也是獨立的,它們之間共享物理內存。進程可以隨便訪問自己的虛擬地址空間,而page table和物理內存由內核維護。當進程需要訪問內存時,CPU會根據進程的page table將虛擬地址翻譯成物理地址,然后進行訪問。

注意:并不是每個虛擬地址空間的page都有對應的Page Table相關聯,只有虛擬地址被分配給進程后,也即進程調用類似malloc函數之后,系統才會為相應的虛擬地址在Page Table中添加記錄,如果進程訪問一個沒有和Page Table關聯的虛擬地址,系統將會拋出SIGSEGV信號,導致進程退出,這也是為什么我們訪問野指針時會經常出現segmentfault的原因。換句話說,雖然每個進程都有4G(32位系統)的虛擬地址空間,但只有向系統申請了的那些地址空間才能用,訪問未分配的地址空間將會出segmentfault錯誤。Linux會將虛擬地址0不映射到任何地方,這樣我們訪問空指針就一定會報segmentfault錯誤。

三、虛擬內存的優點

 ● 更大的地址空間:并且是連續的,使得程序編寫、鏈接更加簡單

 ● 進程隔離:不同進程的虛擬地址之間沒有關系,所以一個進程的操作不會對其它進程造成影響

 ● 數據保護:每塊虛擬內存都有相應的讀寫屬性,這樣就能保護程序的代碼段不被修改,數據塊不能被執行等,增加了系統的安全性

 ● 內存映射:有了虛擬內存之后,可以直接映射磁盤上的文件(可執行文件或動態庫)到虛擬地址空間,這樣可以做到物理內存延時分配,只有在需要讀相應的文件的時候,才將它真正的從磁盤上加載到內存中來,而在內存吃緊的時候又可以將這部分內存清空掉,提高物理內存利用效率,并且所有這些對應用程序來說是都透明的

 ● 共享內存:比如動態庫,只要在內存中存儲一份就可以了,然后將它映射到不同進程的虛擬地址空間中,讓進程覺得自己獨占了這個文件。進程間的內存共享也可以通過映射同一塊物理內存到進程的不同虛擬地址空間來實現共享

 ● 物理內存管理:物理地址空間全部由操作系統管理,進程無法直接分配和回收,從而系統可以更好的利用內存,平衡進程間對內存的需求

 ● 其它:有了虛擬地址空間后,交換空間和COW(copy on write)等功能都能很方便的實現

四、page table

page table可以簡單的理解為一個memory mapping的鏈表(當然實際結構很復雜),里面的每個memory mapping都將一塊虛擬地址映射到一個特定的資源(物理內存或者外部存儲空間)。每個進程擁有自己的page table,和其它進程的page table沒有關系。

五、memory mapping

每個memory mapping就是對一段虛擬內存的描述,包括虛擬地址的起始位置,長度,權限(比如這段內存里的數據是否可讀、寫、執行), 以及關聯的資源(如物理內存page,swap空間上的page,磁盤上的文件內容等)。

當進程申請內存時,系統將返回虛擬內存地址,同時為相應的虛擬內存創建memory mapping并將它放入page table,但這時系統不一定會分配相應的物理內存,系統一般會在進程真正訪問這段內存的時候才會分配物理內存并關聯到相應的memory mapping,這就是所謂的延時分配/按需分配。

每個memory mapping都有一個標記,用來表示所關聯的物理資源類型,一般分兩大類,那就是anonymous和file backed,在這兩大類中,又分了一些小類,比如anonymous下面有更具體的shared和copy on write類型, file backed下面有更具體的device backed類型。下面是每個類型所代表的意思:

file backed

這種類型表示memory mapping對應的物理資源存放在磁盤上的文件中,它所包含的信息包括文件的位置、offset、rwx權限等。

當進程第一次訪問對應的虛擬page的時候,由于在memory mapping中找不到對應的物理內存,CPU會報page fault中斷,然后操作系統就會處理這個中斷并將文件的內容加載到物理內存中,然后更新memory mapping,這樣下次CPU就能訪問這塊虛擬地址了。以這種方式加載到內存的數據一般都會放到page cache中,關于page cache會在后面介紹到.

一般程序的可執行文件,動態庫都是以這種方式映射到進程的虛擬地址空間的。

device backed

和file backed類似,只是后端映射到了磁盤的物理地址,比如當物理內存被swap out后,將被標記為device backed。

anonymous

程序自己用到的數據段和空間,以及通過mmap分配的共享內存,它們在磁盤上找不到對應的文件,所以這部分內存頁被叫做anonymous page。anonymous page和file backed最大的差別是當內存吃緊時,系統會直接刪除掉file backed對應的物理內存,因為下次需要的時候還能從磁盤加載到內存,但anonymous page不能被刪除,只能被swap out。

shared

不同進程的Page Table里面的多個memory mapping可以映射到相同的物理地址,通過虛擬地址(不同進程里的虛擬地址可能不一樣)可以訪問到相同的內容,當一個進程里面修改內存的內容后,在另一個進程中可以立即讀取到。這種方式一般用來實現進程間高速的共享數據(如mmap)。當標記為shared的memory mapping被刪除回收時,需要更新物理page上的引用計數,便于物理page的計數變0后被回收。

copy on write

copy on write基于shared技術,當讀這種類型的內存時,系統不需要做任何特殊的操作,而當要寫這塊內存時,系統將會生成一塊新的內存并拷貝原來內存中的數據到新內存中,然后將新內存關聯到相應的memory mapping,然后執行寫操作。Linux下很多功能都依賴于copy on write技術來提高性能,比如fork等。

通過上面的介紹,我們可以簡單的將內存的使用過程總結如下:

1、進程向系統發出內存申請請求

2、系統會檢查進程的虛擬地址空間是否被用完,如果有剩余,給進程分配虛擬地址

3、系統為這塊虛擬地址創建相應的memory mapping(可能多個),并將它放進該進程的page table

4、系統返回虛擬地址給進程,進程開始訪問該虛擬地址

5、CPU根據虛擬地址在該進程的page table中找到了相應的memory mapping,但是該mapping沒有和物理內存關聯,于是產生缺頁中斷

6、操作系統收到缺頁中斷后,分配真正的物理內存并將它關聯到相應的memory mapping

7、中斷處理完成后,CPU就可以訪問該內存了

當然缺頁中斷不是每次都會發生,只有系統覺得有必要延遲分配內存的時候才用的著,也即很多時候在上面的第3步系統會分配真正的物理內存并和memory mapping關聯。

六、其它概念

操作系統只要實現了虛擬內存和物理內存之間的映射關系,就能正常工作了,但要使內存訪問更高效,還有很多東西需要考慮,在這里我們可以看看跟內存有關的一些其它概念以及它們的作用。

MMU(Memory Management Unit)

MMU是CPU的一個用來將進程的虛擬地址轉換成物理地址的模塊,簡單點說,這個模塊的輸入是進程的page table和虛擬地址,輸出是物理地址。將虛擬地址轉換成物理地址的速度直接影響著系統的速度,所以CPU包含了這個模塊用來加速。

TLB(Translation Lookaside Buffer)

上面介紹到,MMU的輸入是page table,而page table又存在內存里面,跟CPU的cache相比,內存的速度很慢,所以為了進一步加快虛擬地址到物理地址的轉換速度,Linux發明了TLB,它存在于CPU的L1 cache里面,用來緩存已經找到的虛擬地址到物理地址的映射,這樣下次轉換前先查一下TLB,如果已經在里面了就不需要調用MMU了.

按需分配物理頁

由于實際情況下物理內存要比虛擬內存少很多,所以操作系統必須很小心的分配物理內存,以使內存的使用率達到最大化。一個節約物理內存的辦法就是只加載當前正在使用的虛擬page對應的數據到內存。比如,一個很大的數據庫程序,如果你只是用了查詢操作,那么負責插入刪除等部分的代碼段就沒必要加載到內存中,這樣就能節約很多物理內存,這種方法就叫做物理內存頁按需分配,也可以稱作延時加載。

其實現原理很簡單,就是當CPU訪問一個虛擬內存頁的時候,如果這個虛擬內存頁對應的數據還沒加載到物理內存中,則CPU就會通知操作系統發生了page fault,然后由操作系統負責將數據加載進物理內存。由于將數據加載進內存比較耗時,所以CPU不會等在那里,而是去調度其它進程,當它下次再調度到該進程時,數據已經在物理內存上了。

Linux主要使用這種方式來加載可執行文件和動態庫,當程序被內核開始調度執行時,內核將進程的可執行文件和動態庫映射到進程的虛擬地址空間,并只加載馬上要用到的那小部分數據到物理內存中,其它的部分只有當CPU訪問到它們時才去加載。

交換空間

當一個進程需要加載數據到物理內存中,但實際的物理內存已經被用完時,操作系統需要回收一些物理內存中的page以滿足當前進程的需要。

對于file backed的內存數據,即物理內存里面的數據來自于磁盤上的文件,那么內核將直接將該部分數據從內存中移除掉來釋放出更多的內存,當下次有進程需要訪問這部分數據時,再將它從磁盤上加載到內存中來。但是,如果這部分數據被修改過且沒被寫入文件,那這部分數據就變成了臟數據,臟數據不能被直接刪掉,只能被移動到交換空間上去。(可執行文件和動態庫文件不會被修改,但通過mmap+private的方式映射到內存的磁盤文件有可能被修改,這種方式映射的內存比較特殊,沒修改之前是file backed,修改后但沒有寫回磁盤之前就變成了anonymous的)

對于anonymous的內存數據,在磁盤上沒有對應的文件,這部分數據不能直接被刪除,而是被系統移到交換空間上去。交換空間就是磁盤上預留的一塊特殊空間,被系統用來臨時存放內存中不常被訪問的數據,當下次有進程需要訪問交換空間上的數據時,系統再將數據加載到內存中。由于交換空間在磁盤上,所以訪問速度要比內存慢很多,頻繁的讀寫交換空間會帶來性能問題。

關于swap空間的詳細介紹請參考Linux交換空間

共享內存

有了虛擬內存之后,進程間共享內存變得特別的方便。進程所有的內存訪問都通過虛擬地址來實現,而每個進程都有自己的page tables。當兩個進程共享一塊物理內存時,只要將物理內存的頁號映射到兩個進程的page table中就可以了,這樣兩個進程就可以通過不同的虛擬地址來訪問同一塊物理內存。

從上面的那個圖中可以看出,進程X和進程Y共享了物理內存頁PFN3,在進程X中,PFN3被映射到了VPFN3,而在進程Y中,PFN3被映射到了VPFN1,但兩個進程通過不同的虛擬地址訪問到的物理內存是同一塊。

訪問控制

page table里面的每條虛擬內存到物理內存的映射記錄(memory mapping)都包含一份控制信息,當進程要訪問一塊虛擬內存時,系統可以根據這份控制信息來檢查當前的操作是否是合法的。

為什么需要做這個檢查呢?比如有些內存里面放的是程序的可執行代碼,那么就不應該去修改它;有些內存里面存放的是程序運行時用到的數據,那么這部分內存只能被讀寫,不應該被執行;有些內存里面存放的是內核的代碼,那么在用戶態就不應該去執行它;有了這些檢查之后會大大增強系統的安全性。

huge pages

由于CPU的cache有限,所以TLB里面緩存的數據也有限,而采用了huge page后,由于每頁的內存變大(比如由原來的4K變成了4M),雖然TLB里面的紀錄數沒變,但這些紀錄所能覆蓋的地址空間變大,相當于同樣大小的TLB里面能緩存的映射范圍變大,從而減少了調用MMU的次數,加快了虛擬地址到物理地址的轉換速度。

Caches

為了提高系統性能,Linux使用了一些跟內存管理相關的cache,并且盡量將空閑的內存用于這些cache。這些cache都是系統全局共享的:

  • Buffer Cache
    用來緩沖塊設備上的數據,比如磁盤,當讀寫塊設備時,系統會將相應的數據存放到這個cache中,等下次再訪問時,可以直接從cache中拿數據,從而提高系統效率。它里面的數據結構是一個塊設備ID和block編號到具體數據的映射,只要根據塊設備ID和塊的編號,就能找到相應的數據。

  • Page Cache
    這個cache主要用來加快讀寫磁盤上文件的速度。它里面的數據結構是文件ID和offset到文件內容的映射,根據文件ID和offset就能找到相應的數據(這里文件ID可能是inode或者path,本人沒有仔細去研究)。

從上面的定義可以看出,page cache和buffer cache有重疊的地方,不過實際情況是buffer cache只緩存page cache不緩存的那部分內容,比如磁盤上文件的元數據。所以一般情況下和page cache相比,Buffer Cache的大小基本可以忽略不計。

當然,使用cache也有一些不好的地方,比如需要時間和空間去維護cache,cache一旦出錯,整個系統就掛了。

七、總結

有了上面介紹的知識,再來看看我們剛開始提出來的問題,以top命令的輸出為例:

KiB Mem :   500192 total,   349264 free,    36328 used,   114600 buff/cache KiB Swap:   524284 total,   524284 free,        0 used.   433732 avail Mem

KiB Mem代表物理內存,KiB Swap代表交換空間,它們的單位都是KiB。

total、used和free沒什么好介紹的,就是總共多少,然后用了多少,還剩多少。

buff/cached代表了buff和cache總共用了多少,buff代表buffer cache占了多少空間,由于它主要用來緩存磁盤上文件的元數據,所以一般都比較小,跟cache比可以忽略不計;cache代表page cache和其它一些占用空間比較小且大小比較固定的cache的總和,基本上cache就約等于page cache,page cache的準確值可以通過查看/proc/meminf中的Cached得到。由于page cache是用來緩存磁盤上文件內容的,所以占有空間很大,Linux一般會盡可能多的將空閑物理內存用于page cache。

avail Mem表示可用于進程下一次分配的物理內存數量,這個大小一般比free大一點,因為除了free的空間外,系統還能立即釋放出一些空間來。

那么怎么判斷當前內存使用情況出現了異常呢?有下面幾點供參考:

● Mem free的值比較小,并且buff/cache的值也小
free的值比較少并不一定代表有問題,因為Linux會盡可能多的將內存用于page cache,但是如果buff/cache的值也小,就說明內存吃緊了,系統沒有足夠多的內存用于cache,如果當前服務器部署是一個需要頻繁的讀寫磁盤的應用,如FTP服務器,那么對性能的影響將會非常大。

● Swap used的值比較大,
這種情況比上面的更嚴重,正常情況下swap應該很少被使用,used值比較大說明交換空間被使用的比較多,如果通過vmstat命令看到swap in/out的比較頻繁的話,說明系統內存嚴重不足,整體性能已經受到嚴重影響

相關視頻教程推薦:《Linux教程

? 版權聲明
THE END
喜歡就支持一下吧
點贊6 分享