前一段時間看了《深刻理解Linux內核》對其中的內存管理部分花了很多時間,可是仍是有不少問題不是很清楚,最近又花了一些時間複習了一下,在這裏記錄下本身的理解和對Linux中內存管理的一些見解和認識。 算法
我比較喜歡搞清楚一個技術自己的發展歷程,簡而言之就是這個技術是怎麼發展而來的,在這個技術以前存在哪些技術,這些技術有哪些特色,爲何會被目前的技術所取代,而目前的技術又解決了以前的技術所存在的哪些問題。弄清楚了這些,咱們才能比較清晰的把握某一項技術。有些資料在介紹某個概念的時候直接就介紹這個概念的意義,原理,而對其發展過程和背後的原理絲絕不提,彷彿這個技術從天上掉下來的同樣。介於此,仍是之內存管理的發展歷程來說述今天的主題。 安全
首先,我必需要闡述一下這篇文章的主題是Linux內存管理中的分段和分頁技術。 函數
讓咱們來回顧一下歷史,在早期的計算機中,程序是直接運行在物理內存上的。換句話說,就是程序在運行的過程當中訪問的都是物理地址。若是這個系統只運行一個程序,那麼只要這個程序所需的內存不要超過該機器的物理內存就不會出現問題,咱們也就不須要考慮內存管理這個麻煩事了,反正就你一個程序,就這麼點內存,吃不吃得飽那是你的事情了。然而如今的系統都是支持多任務,多進程的,這樣CPU以及其餘硬件的利用率會更高,這個時候咱們就要考慮到將系統內有限的物理內存如何及時有效的分配給多個程序了,這個事情自己咱們就稱之爲內存管理。 進程
下面舉一個早期的計算機系統中,內存分配管理的例子,以便於你們理解。 內存
加入咱們有三個程序,程序1,2,3.程序1運行的過程當中須要10M內存,程序2運行的過程當中須要100M內存,而程序3運行的過程當中須要20M內存。若是系統同時須要運行程序A和B,那麼早期的內存管理過程大概是這樣的,將物理內存的前10M分配給A, 接下來的10M-110M分配給B。這種內存管理的方法比較直接,好了,假設咱們這個時候想讓程序C也運行,同時假設咱們系統的內存只有128M,顯然按照這種方法程序C因爲內存不夠是不可以運行的。你們知道可使用虛擬內存的技術,內存空間不夠的時候能夠將程序不須要用到的數據交換到磁盤空間上去,已達到擴展內存空間的目的。下面咱們來看看這種內存管理方式存在的幾個比較明顯的問題。就像文章一開始提到的,要很深層次的把握某個技術最好搞清楚其發展歷程。 it
1.進程地址空間不能隔離 內存管理
因爲程序直接訪問的是物理內存,這個時候程序所使用的內存空間不是隔離的。舉個例子,就像上面說的A的地址空間是0-10M這個範圍內,可是若是A中有一段代碼是操做10M-128M這段地址空間內的數據,那麼程序B和程序C就極可能會崩潰(每一個程序均可以系統的整個地址空間)。這樣不少惡意程序或者是木馬程序能夠垂手可得的破快其餘的程序,系統的安全性也就得不到保障了,這對用戶來講也是不能容忍的。 io
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),他就是專門負責從虛擬地址到物理地址轉換的,也就是從虛擬頁找到物理頁。