linux 記憶體管理) 分析得很好,有自己理解的東西

來源:互聯網
上載者:User

前言
記憶體管理一向是所有作業系統書籍不惜筆墨重點討論的內容,無論市面上或是網上都充斥著大量涉及記憶體管理的教材和資料。因此我們這裡所要寫的Linux記憶體管理採取必重就輕的策略,從理論層面就不去板門弄斧,貽笑大方了。我們最想做的和可能做到的是以開發人員的角度談談對記憶體管理的理解,最終目的是把我們在核心開發中使用記憶體的經驗和對Linux記憶體管理的認識與大家共用。

當然這其中我們也會設計一些諸如段頁等記憶體管理的基本理論,但我們目的不是為了強調理論,而是為了指導理解開發中的實踐,所以僅僅點到為止,不做深究。

遵循“理論來源於實踐”的“教條”,我們先不必一下子就鑽入核心裡去看系統記憶體到底是如何管理,那樣往往會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。所以最好的方式是先從外部(使用者編程範疇)來觀察進程如何使用記憶體,等到對大家記憶體使用量有了較直觀的認識後,再深入到核心中去學習記憶體如何被管理等理論知識。最後再通過一個執行個體編程將所講內容融會貫通。

進程與記憶體
進程如何使用記憶體?
毫無疑問所有進程(執行的程式)都必須佔用一定數量的記憶體,它或是用來存放從磁碟載入的程式碼,或是存放取自使用者輸入的資料等等。不過進程對這些記憶體的管理方式因記憶體用途不一而不盡相同,有些記憶體是事先靜態分配和統一回收的,而有些卻是按需要動態分配和回收的。

對任何一個普通進程來講,它都會涉及到5種不同的資料區段。稍有編程知識的朋友都該能想到這幾個資料區段種包含有“程式碼段”、“程式資料區段”、“程式堆棧段”等。不錯,這幾種資料區段都在其中,但除了以上幾種資料區段之外,進程還另外包含兩種資料區段。下面我們來簡單歸納一下進程對應的記憶體空間中所包含的5種不同的資料區。

程式碼片段:程式碼片段是用來存放可執行檔的操作指令,也就是說是它是可執行程式在記憶體種的鏡像。程式碼片段需要防止在運行時被非法修改,所以只准許讀取操作,而不允許寫入(修改)操作——它是不可寫的。

資料區段:資料區段用來存放可執行檔中已初始化全域變數,換句話說就是存放程式靜態分配[1]的變數和全域變數。

BSS段[2]:BSS段包含了程式中未初始化全域變數,在記憶體中 bss段全部置零。

堆(heap):堆是用於存放進程運行中被動態分配的記憶體段,它大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配記憶體時,新分配的記憶體就被動態添加到堆上(堆被擴張);當利用free等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)

棧:棧是使用者存放程式臨時建立的局部變數,也就是說我們函數括弧“{}”中定義的變數(但不包括static聲明的變數,static意味這在資料區段中存放變數)。除此以外在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的傳回值也回被存放回棧中。由於棧的先進先出特點,所以棧特別方便用來儲存/恢複調用現場。從這個意義上將我們可以把堆棧看成一個臨時資料寄存、交換的記憶體區。

 

[1] 靜態分配記憶體就是編譯器在編譯器的時候根據來源程式來分配記憶體. 動態分配記憶體就是在程式編譯之後, 運行時調用運行時刻庫函數來分配記憶體的. 靜態分配由於是在程式運行之前,所以速度快, 效率高, 但是局限性大. 動態分配在程式運行時執行, 所以速度慢, 但靈活性高.

[2]術語"BSS"已經有些年頭了,它是block started by symbol的縮寫。因為未初始化的變數沒有對應的值,所以並不需要儲存在可執行對象中。但是因為C標準強制規定未初始化的全域變數要被賦予特殊的預設值(基本上是0值),所以核心要從可執行代碼裝入變數(未賦值的)到記憶體中,然後將零頁映射到該片記憶體上,於是這些未初始設定變數就被賦予了0值。這樣做避免了在目標檔案中進行顯式地初始化,減少空間浪費(來自《Linux核心開發》)

 

進程如何組織這些地區?
上述幾種記憶體地區中資料區段、BSS和堆通常是被連續儲存的——記憶體位置上是連續的,而程式碼片段和棧往往會被獨立存放。有趣的是堆和棧兩個地區關係很“曖昧”,他們一個向下“長”(i386體繫結構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程式計算一下),絕少有機會能碰到一起。

簡要描述了進程記憶體地區的分布:

實是已鏈表形式連結,不過位了方便尋找,核心又以紅/黑樹狀結構(以前的核心使用平衡樹)的形式組織記憶體地區,以便降低搜尋耗時。並存兩種組織形式,並非冗餘:鏈表用於需要遍曆全部節點的時候用,而紅/黑樹狀結構適用於在地址空間中定位特定記憶體地區的時候。核心為了記憶體地區上的各種不同操作都能獲得高效能,所以同時使用了這兩種資料結構。

反映了進程地址空間的管理模型:

 

“事實勝於雄辯”,我們用一個小例子(原形取自《User-Level Memory Management》)來展示上面所講的各種記憶體區的差別與位置。

#include<stdio.h>

#include<malloc.h>

#include<unistd.h>

int bss_var;

int data_var0=1;

int main(int argc,char **argv)

{

printf("below are addresses of types of process's mem/n");

printf("Text location:/n");

printf("/tAddress of main(Code Segment):%p/n",main);

printf("____________________________/n");

int stack_var0=2;

printf("Stack Location:/n");

printf("/tInitial end of stack:%p/n",&stack_var0);

int stack_var1=3;

printf("/tnew end of stack:%p/n",&stack_var1);

printf("____________________________/n");

printf("Data Location:/n");

printf("/tAddress of data_var(Data Segment):%p/n",&data_var0);

static int data_var1=4;

printf("/tNew end of data_var(Data Segment):%p/n",&data_var1);

printf("____________________________/n");

printf("BSS Location:/n");

printf("/tAddress of bss_var:%p/n",&bss_var);

printf("____________________________/n");

char *b = sbrk((ptrdiff_t)0);

printf("Heap Location:/n");

printf("/tInitial end of heap:%p/n",b);

brk(b+4);

b=sbrk((ptrdiff_t)0);

printf("/tNew end of heap:%p/n",b);

return 0;

}

它的結果如下

below are addresses of types of process's mem

Text location:

   Address of main(Code Segment):0x8048388

____________________________

Stack Location:

   Initial end of stack:0xbffffab4

   new end of stack:0xbffffab0

____________________________

Data Location:

   Address of data_var(Data Segment):0x8049758

   New end of data_var(Data Segment):0x804975c

____________________________

BSS Location:

   Address of bss_var:0x8049864

____________________________

Heap Location:

   Initial end of heap:0x8049868

   New end of heap:0x804986c

利用size命令也可以看到程式的各段大小,比如執行size example會得到

text data bss dec hex filename

1654 280   8 1942 796 example

但這些資料是程式編譯的靜態統計,而上面顯示的是進程運行時動態值,但兩者是對應的。

從前面的例子,我們對進程使用的邏輯記憶體分布已經先睹為快。這部分我們就繼續進入作業系統核心看看進程對記憶體具體是如何進行分配和管理的。

從使用者向核心看,所使用的記憶體表象形式會依次經曆“邏輯地址”——“線形地址”——“物理地址”幾種形式(關於幾種地址的解釋在前面已經講述了)。邏輯地址經段機制轉化成線性地址;線性地址又經過頁機制轉化為物理地址。(但是我們要知道Linux系統雖然保留了段機制,但是將所有程式的段地址都定死為0-4G,所以雖然邏輯地址和線性地址是兩種不同的地址空間,但在Linux中邏輯地址就等於線性地址,它們的值是一樣的)。沿著這條線索,我們所研究的主要問題也就集中在下面幾個問題。

1.         進程空間地址如何管理?

2.         進程地址如何映射到實體記憶體?

3.         實體記憶體如何被管理?

以及由上述問題引發的一些子問題。如系統虛擬位址分布;記憶體配置介面;連續記憶體配置與非連續記憶體配置等。

進程記憶體空間
Linux作業系統採用虛擬記憶體管理技術,使得每個進程都有各自互不干涉的進程地址空間。該空間是塊大小為4G的線性虛擬空間,使用者所看到和接觸的都是該虛擬位址,無法看到實際的實體記憶體地址。利用這種虛擬位址不但能起到保護作業系統的效果(使用者不能直接存取實體記憶體),而且更重要的是使用者程式可使用比實際實體記憶體更大的地址空間(具體的原因請看硬體基礎部分)。

在討論進程空間細節前,請大家這裡先要澄清下面幾個問題。

l         第一、4G的進程地址空間被人為的分為兩個部分——使用者空間與核心空間。使用者空間從0到3G(0xC0000000),核心空間佔據3G到4G。使用者進程通常情況下只能訪問使用者空間的虛擬位址,不能訪問核心空間虛擬位址。例外情況只有使用者進程進行系統調用(代表使用者進程在核心態執行)等時刻可以訪問到核心空間。

l         第二、使用者空間對應進程,所以每當進程切換,使用者空間就會跟著變化;而核心空間是由核心負責映射,它並不會跟著進程改變,是固定的。核心空間地址有自己對應的頁表(init_mm.pgd),使用者進程各自有不同的頁表(。

l         第三、每個進程的使用者空間都是完全獨立、互不相干的。不信的話,你可以把上面的程式同時運行10次(當然為了同時運行,讓它們在返回前一同睡眠100秒吧),你會看到10個進程佔用的線性地址一模一樣。

進程記憶體管理
進程記憶體管理的對象是進程線性地址空間上的記憶體鏡像,這些記憶體鏡像其實就是進程使用的虛擬記憶體地區(memory region)。進程虛擬空間是個32或64位的“平坦”(獨立的連續區間)地址空間(空間的具體大小取決於體繫結構)。要統一管理這麼大的平坦空間可絕非易事,為了方便管理,虛擬空間被化分為許多大小可變的(但必須是4096的倍數)記憶體地區,這些地區在進程線性地址中像停車位一樣有序排列。這些地區的劃分原則是“將訪問屬性一致的地址空間存放在一起”,所謂訪問屬性在這裡無非指的是“可讀、可寫、可執行等”。

如果你要查看某個進程佔用的記憶體地區,可以使用命令cat /proc/<pid>/maps獲得(pid是進程號,你可以運行上面我們給出的例子——./example &;pid便會列印到螢幕),你可以發現很多類似於下面的數字資訊。

由於程式example使用了動態庫,所以除了example本身使用的的記憶體地區外,還會包含那些動態庫使用的記憶體地區(地區順序是:程式碼片段、資料區段、bss段)。

我們下面只抽出和example有關的資訊,除了前兩行代表的程式碼片段和資料區段外,最後一行是進程使用的棧空間。

-------------------------------------------------------------------------------

08048000 - 08049000 r-xp 00000000 03:03 439029                               /home/mm/src/example

08049000 - 0804a000 rw-p 00000000 03:03 439029                               /home/mm/src/example

……………

bfffe000 - c0000000 rwxp ffff000 00:00 0

----------------------------------------------------------------------------------------------------------------------

每行資料格式如下:

(記憶體地區)開始-結束存取權限 位移 主裝置號:次裝置號 i節點 檔案。

注意,你一定會發現進程空間只包含三個記憶體地區,似乎沒有上面所提到的堆、bss等,其實並非如此,程式記憶體段和進程地址空間中的記憶體地區是種模糊對應,也就是說,堆、bss、資料區段(初始化過的)都在進程空間種由資料區段記憶體地區表示。

在Linux核心中對應進程記憶體地區的資料結構是: vm_area_struct, 核心將每個記憶體地區作為一個單獨的記憶體對象管理,相應的操作也都一致。採用物件導向方法使VMA結構體可以代表多種類型的記憶體地區--比如記憶體對應檔或進程的使用者空間棧等,對這些地區的操作也都不盡相同。

vm_area_strcut結構比較複雜,關於它的詳細結構請參閱相關資料。我們這裡只對它的組織方法做一點補充說明。vm_area_struct是描述進程地址空間的基本嵌入式管理單元,對於一個進程來說往往需要多個記憶體地區來描述它的虛擬空間,如何關聯這些不同的記憶體地區呢?大家可能都會想到使用鏈表,的確vm_area_struct結構確實是已鏈表形式連結,不過位了方便尋找,核心又以紅/黑樹狀結構(以前的核心使用平衡樹)的形式組織記憶體地區,以便降低搜尋耗時。並存兩種組織形式,並非冗餘:鏈表用於需要遍曆全部節點的時候用,而紅/黑樹狀結構適用於在地址空間中定位特定記憶體地區的時候。核心為了記憶體地區上的各種不同操作都能獲得高效能,所以同時使用了這兩種資料結構。

反映了進程地址空間的管理模型:


 
  進程記憶體描述符
Vm_area_struct
  進程虛擬位址
進程的地址空間對應的描述結構是“記憶體描述符結構”,它表示進程的全部地址空間,——包含了和進程地址空間有關的全部資訊,其中當然包含進程的記憶體地區。

 

 

進程記憶體的分配與回收
建立進程fork()、程式載入execve()、對應檔mmap()、動態記憶體分配malloc()/brk()等進程相關操作都需要分配記憶體給進程。不過這時進程申請和獲得的還不是實際記憶體,而是虛擬記憶體,準確的說是“記憶體地區”。進程對記憶體地區的分配最終多會歸結到do_mmap()函數上來(brk調用被單獨以系統調用實現,不用do_mmap()),

核心使用do_mmap()函數建立一個新的線性地址區間。但是說該函數建立了一個新VMA並不非常準確,因為如果建立的地址區間和一個已經存在的地址區間相鄰,並且它們具有相同的存取權限的話,那麼兩個區間將合并為一個。如果不能合并,那麼就確實需要建立一個新的VMA了。但無論哪種情況, do_mmap()函數都會將一個地址區間加入到進程的地址空間中--無論是擴充已存在的記憶體地區還是建立一個新的地區。

同樣釋放一個記憶體地區使用函數do_ummap(),它會銷毀對應的記憶體地區。

如何由虛變實!
    從上面已經看到進程所能直接操作的地址都為虛擬位址。當進程需要記憶體時,從核心獲得的僅僅時虛擬記憶體地區,而不是實際的物理地址,進程並沒有獲得實體記憶體(物理頁框——頁的概念請大家參與硬體基礎一章),獲得的僅僅是對一個新的線性地址區間的使用權。實際的實體記憶體只有當進程真的去訪問新擷取的虛擬位址時,才會由“請頁機制”產生“缺頁”異常,從而進入分配實際頁框的常式。

該異常是虛擬記憶體機制賴以存在的基本保證——它會告訴核心去真正為進程分配物理頁,並建立對應的頁表,這之後虛擬位址才實實在在映射到了系統實體記憶體上。(當然如果頁被換出到磁碟,也會產生缺頁異常,不過這時不用再建立頁表了)

這種請頁機制把頁框的分配延遲到不能再延遲為止,並不急於把所有的事情都一次做完(這中思想由點想涉及模式中的代理模式(proxy))。之所以能這麼做是利用了記憶體訪問的“局部性原理”,請頁帶來的好處是節約了空閑記憶體,提高了系統吞吐。要想更清楚的瞭解請頁,可以看看《深入理解linux核心》一書。

這裡我們需要說明在記憶體地區結構上的nopage操作,該操作是當發生訪問的進程虛擬記憶體而發現並未真正分配頁框時,該方法變被調用來分配實際的物理頁,並為該頁建立頁表項。在最後的例子中我們會示範如何使用該方法。

 

系統實體記憶體管理
雖然應用程式操作的對象是映射到實體記憶體之上的虛擬記憶體,但是處理器直接操作的卻是實體記憶體。所以當用程式訪問一個虛擬位址時,首先必須將虛擬位址轉化成物理地址,然後處理器才能解析地址訪問請求。地址的轉換工作需要通過查詢頁表才能完成,概括的講,地址轉換需要將虛擬位址分段,使每段虛地址都作為一個索引指向頁表,而頁表項則指向下一層級的頁表或者指向最終的物理頁面。

每個進程都有自己的頁表。進程描述符號的pgd域指向的就是進程的頁全域目錄。席面我們借用《linux裝置驅動程式》中的一幅圖大致看看進程地址空間到物理頁之間的轉換關係。

 

     上面的過程說起簡單,做起難呀。因為在虛擬位址映射到頁之前必須先分配物理頁——也就是說必須先從核心擷取空閑頁,並建立頁表。下面我們介紹一下核心管理實體記憶體的機制。

實體記憶體管理(頁管理)

Linux核心管理實體記憶體是通過分頁機制實現的,它將整個記憶體劃分成無數4k(在i386體繫結構中)大小頁,從而分配和回收記憶體的基本單位便是記憶體頁了。利用分頁管理有助於靈活分配記憶體位址,因為分配時不必要求必須有大塊的連續記憶體[1],系統可以東一頁、西一頁的湊出所需要的記憶體供進程使用。雖然如此,但是實際上系統使用記憶體還是傾向於分配連續的記憶體塊,因為分配連續記憶體時,頁表不需要更改,因此能降低TLB的重新整理率(頻繁重新整理會很大增加訪問速度)。

鑒於上述需求,核心分配物理頁為了盡量減少不連續情況,採用了“夥伴”關係來管理空閑頁框。夥伴關係分配演算法大家不應陌生——幾乎所有作業系統書都會提到,我們不去詳細說它了,如果不明白可以參看有關資料。這裡只需要大家明白Linux中空閑頁面的組織和管理利用了夥伴關係,因此空閑頁面分配時也需要遵循夥伴關係,最小單位只能是2的冪倍頁面大小。核心中分配空閑頁框的基本函數是get_free_page/get_free_pages,它們或是分配單頁或是分配指定的頁框(2、4、8…512頁)。

注意:get_free_page是在核心中分配記憶體,不同於malloc在使用者空間中分配,malloc利用堆動態分配,實際上是調用brk()系統調用,該調用的作用是擴大或縮小進程堆空間(它會修改進程的brk域)。如果現有的記憶體地區不夠容納堆空間,則會以頁面大小的倍數位單位,擴張或收縮對應的記憶體地區,但brk值並非以頁面大小為倍數修改,而是按實際請求修改。因此Malloc在使用者空間分配記憶體可以以位元組為單位分配,但核心在內部仍然會是以頁為單位分配的。

   另外需要提及的是,物理頁在系統中由頁框結構struct paga描述,系統中所有的頁框儲存在數組mem_map[]中,可以通過該數組找到系統中的每一頁(空閑或非空閑)。而其中的空閑頁框則可由上述提到的以夥伴關係組織的空閑頁鏈表(free_area[MAX_ORDER])索引。

核心記憶體使用量

Slab

    所謂尺有所長,寸有所短。以頁為最小單位分配記憶體對於核心管理系統實體記憶體來說的確比較方便,但核心自身最常使用的記憶體卻往往是很小(遠遠小於一頁)的記憶體塊——比如存放檔案描述符、進程描述符、虛擬記憶體地區描述符等行為所需的記憶體都不足一頁。這些用來存放描述符的記憶體相比頁面而言,就好比是麵包屑與麵包。一個整頁中可以聚集多個這種這些小塊記憶體;而且這些小塊記憶體塊也和麵包屑一樣頻繁地產生/銷毀。

為了滿足核心對這種小記憶體塊的需要,Linux系統採用了一種被稱為slab分配器的技術。Slab分配器的實現相當複雜,但原理不難,其核心思想就是“儲存池[2]”的運用。記憶體片段(小塊記憶體)被看作對象,當被使用完後,並不直接釋放而是被緩衝到“儲存池”裡,留做下次使用,這無疑避免了頻繁建立與銷毀對象所帶來的額外負載。

Slab技術不但避免了記憶體內部分區(下文將解釋)帶來的不便(引入Slab分配器的主要目的是為了減少對夥伴系統分配演算法的調用次數——頻繁分配和回收必然會導致記憶體片段——難以找到大塊連續的可用記憶體),而且可以很好利用硬體緩衝提高訪問速度。

   Slab並非是脫離夥伴關係而獨立存在的一種記憶體配置方式,slab仍然是建立在頁面基礎之上,換句話說,Slab將頁面(來自於夥伴關係管理的空閑頁框鏈)撕碎成眾多小記憶體塊以供分配,slab中的對象分配和銷毀使用kmem_cache_alloc與kmem_cache_free。

Kmalloc

Slab分配器不僅僅只用來存放核心專用的結構體,它還被用來處理核心對小塊記憶體的請求。當然鑒於Slab分配器的特點,一般來說核心程式中對小於一頁的小塊記憶體的求情才通過Slab分配器提供的介面Kmalloc來完成(雖然它可分配32 到131072位元組的記憶體)。從核心記憶體配置角度講kmalloc可被看成是get_free_page(s)的一個有效補充,記憶體配置粒度更靈活了。

有興趣的話可以到/proc/slabinfo中找到核心執行現場使用的各種slab資訊統計,其中你會看到系統中所有slab的使用資訊。從資訊中可以看到系統中除了專用結構體使用的slab外,還存在大量為Kmalloc而準備的Slab(其中有些為dma準備的)。

1] 還有些情況必須要求記憶體連續,比如DMA傳輸中使用的記憶體,由於不涉及頁機制所以必須連續分配。

[2] 這種儲存池的思想在電腦科學裡廣泛應用,比如資料庫連接池、記憶體訪問池等等

核心非連續記憶體配置(Vmalloc)

夥伴關係也好、slab技術也好,從記憶體管理理論角度而言目的基本是一致的,它們都是為了防止“分區”,不過分區又分為外部分區和內部分區之說,所謂內部分區是說系統為了滿足一小段記憶體區(連續)的需要,不得不分配了一大地區連續記憶體給它,從而造成了空間浪費;外部分區是指系統雖有足夠的記憶體,但卻是分散的片段,無法滿足對大塊“連續記憶體”的需求。無論何種分區都是系統有效利用記憶體的障礙。slab分配器使得含與一個頁面內眾多小塊記憶體可獨立被分配使用,避免了內部分區,節約了空閑記憶體。夥伴關係把記憶體塊按大小分組管理,一定程度上減輕了外部分區的危害,因為頁框分配不在盲目,而是按照大小依次有序進行,不過夥伴關係只是減輕了外部分區,但並未徹底消除。你自己筆畫一下多次分配頁框後,空閑記憶體的剩餘情況吧。

所以避免外部分區的最終思路還是落到了如何利用不連續的記憶體塊組合成“看起來很大的記憶體塊”——這裡的情況很類似於使用者空間分配虛擬記憶體,記憶體邏輯上連續,其實影射到並不一定連續的實體記憶體上。Linux核心借用了這個技術,允許核心程式在核心地址空間中分配虛擬位址,同樣也利用頁表(核心頁表)將虛擬位址影射到分散的記憶體頁上。以此完美地解決了核心記憶體使用量中的外部分區問題。核心提供vmalloc函數分配核心虛擬記憶體,該函數不同於kmalloc,它可以分配較Kmalloc大得多的記憶體空間(可遠大於128K,但必須是頁大小的倍數),但相比Kmalloc來說Vmalloc需要對核心虛擬位址進行重影射,必須更新核心頁表,因此分配效率上要低一些(用空間換時間)

與使用者進程相似核心也有一個名為init_mm的mm_strcut結構來描述核心地址空間,其中頁表項pdg=swapper_pg_dir包含了系統核心空間(3G-4G)的映射關係。因此vmalloc分配核心虛擬位址必須更新核心頁表,而kmalloc或get_free_page由於分配的連續記憶體,所以不需要更新核心頁表。

vmalloc分配的核心虛擬記憶體與kmalloc/get_free_page分配的核心虛擬記憶體位於不同的區間,不會重疊。因為核心虛擬空間被分區管理,各司其職。進程空間地址分布從0到3G(其實是到PAGE_OFFSET,在0x86中它等於0xC0000000),從3G到vmalloc_start這段地址是實體記憶體映射地區(該地區中包含了核心鏡像、物理頁框表mem_map等等)比如我使用的系統記憶體是64M(可以用free看到),那麼(3G——3G+64M)這片記憶體就應該映射實體記憶體,而vmalloc_start位置應在3G+64M附近(說附近因為是在實體記憶體映射區與vmalloc_start期間還回存在一個8M大小的gap來防止躍界),vmalloc_end的位置接近4G(說接近是因為最後位置系統會保留一片128k大小的地區用於專用頁面映射,還由可能會由高端記憶體映射區,這些都是細節,這裡我們不做糾纏)。

是記憶體分布的模糊輪廓
  由get_free_page或Kmalloc函數所分配的連續記憶體都陷於物理映射地區,所以它們返回的核心虛擬位址和實際物理地址僅僅是相差一個位移量(PAGE_OFFSET),你可以很方便的將其轉化為實體記憶體地址,同時核心也提供了virt_to_phys()函數將核心虛擬空間中的物理影射區地址轉化為物理地址。要知道,實體記憶體映射區中的地址與核心頁表是有序對應,系統中的每個物理頁框都可以找到它對應的核心虛擬位址(在實體記憶體映射區中的)。

而vmalloc分配的地址則限於vmalloc_start與vmalloc_end之間。每一塊vmalloc分配的核心虛擬記憶體都對應一個vm_struct結構體(可別和vm_area_struct搞混,那可是進程虛擬記憶體地區的結構),不同的核心虛擬位址被4k打大小空閑區的間隔,以防止越界——見)。與進程虛擬位址的特性一樣,這些虛擬位址可與實體記憶體沒有簡單的位移關係,必須通過核心頁表才可轉換為物理地址或物理頁。它們有可能尚未被映射,在發生缺頁時才真正分配物理頁框。

 

這裡給出一個小程式協助大家認請上面幾種分配函數所對應的地區。

#include<linux/module.h>

#include<linux/slab.h>

#include<linux/vmalloc.h>

unsigned char *pagemem;

unsigned char *kmallocmem;

unsigned char *vmallocmem;

int init_module(void)

{

pagemem = get_free_page(0);

printk("<1>pagemem=%s",pagemem);

kmallocmem = kmalloc(100,0);

printk("<1>kmallocmem=%s",kmallocmem);

vmallocmem = vmalloc(1000000);

printk("<1>vmallocmem=%s",vmallocmem);

}

void cleanup_module(void)

{

free_page(pagemem);

kfree(kmallocmem);

vfree(vmallocmem);

}

 

 

記憶體管理執行個體
代碼功能介紹
我們希望能通過訪問使用者空間的記憶體達到讀取核心資料的目的,這樣便可進行核心空間到使用者空間的大規模資訊傳輸。

具體的講,我們要利用記憶體映射功能,將系統核心中的一部分虛擬記憶體映射到使用者空間,從而使得使用者空間地址等同與被映射的核心記憶體位址。

代碼結構體系介紹
核心空間記憶體配置介紹
因此我們將試圖寫一個虛擬字元裝置驅動程式,通過它將系統核心空間映射到使用者空間——將核心虛擬記憶體映射到使用者虛擬位址。當然映射地址時少不了定位核心空間對應的物理地址,並且還要建立新的使用者頁表項,以便使用者進程定址時能找到對應的實體記憶體。

從中應該看出,需要我完成既定目標,我們需要獲得:被映射核心空間物理地址 和 建立對應的使用者進程頁表。

在核心空間中主要存在kmalloc分配的物理連續空間和vmalloc分配的非物理連續空間。kmalloc分配的空間往往被稱為核心邏輯地址,由於它是連續分配(直接處理物理頁框),而且分配首地址一定,所以其分配的核心虛擬位址對應的實際物理地址很容易獲得:核心虛擬位址—PAGE_OFFSET(0xC0000000)(核心有對應常式virt_to_phys)即等於物理地址,而且其對應的頁表屬於核心頁表(swapper_pg_dir)——在系統初始化時就以建立,因此省去了建立頁表的工作。

而vmalloc分配的空間被稱為核心虛擬位址,它的問題相對要複雜些,這是因為其分配的核心虛擬記憶體空間並非直接操作頁框,而是分配的是vm_struct結構。該結構邏輯上連續但對應的實體記憶體並非連續,也就是說它vamlloc分配的核心空間地址所對應的物理地址並非可通過簡單線性運算獲得。從這個意義上講,它的物理地址在分配前是不確定的,因此雖然vmalloc分配的空間與kmalloc一樣都是由核心頁表來映射的,但vmalloc分配核心虛擬位址時必須更新核心頁表

注釋:vmalloc分配的核心虛擬記憶體與kmalloc/get_free_page分配的核心邏輯記憶體位於不同的區間,不會重疊。因為核心空間被分區管理,各司其職。進程空間地址分布從0到3G(其實是到PAGE_OFFSET,在0x86中它等於0xC0000000),從3G到vmalloc_start這段地址是實體記憶體映射地區(該地區中包含了核心鏡像、物理頁框表mem_map等等)比如我使用的系統記憶體是64M(可以用free看到),那麼(3G——3G+64M)這片記憶體就應該映射實體記憶體,而vmalloc_start位置應在3G+64M附近(說附近因為是在實體記憶體映射區與vmalloc_start期間還回存在一個8M大小的gap來防止躍界),vmalloc_end的位置接近4G(說接近是因為最後位置系統會保留一片128k大小的地區用於專用頁面映射,還由可能會由高端記憶體映射區,這些都是細節,這裡我們不做糾纏)。

       另一個需要澄清的是,vmalloc分配的核心空間,其結構是vm_area,可千萬別與使用者空間malloc分配的vm_area_struct結構混淆。前者由核心頁表映射,而後者則由使用者頁表映射。

進程地址空間
實體記憶體映射區kmalloc分配
Vmalloc 分配區

3G(page_offset)
核心虛擬空間 
Vmalloc_start
Vmalloc_end


是記憶體分布的模糊輪廓

為了近可能豐富我們的例子程式的情境,我們選擇映射vmalloc分配的核心虛擬空間(下面我們簡稱為vk地址)到使用者空間。

要知道使用者進程操作的是虛擬記憶體地區vm_area_struct,我們此刻需要將使用者vma區間利用使用者頁表映射到vk對應的實體記憶體上去(如所示)。這裡主要工作便是建立使用者也表項完成映射工作,而這個工作完全落在了vma->nopage[3]操作上,該方法會協助我們在發生“缺頁”時,動態構造映射所需實體記憶體的頁表項。

使用者虛擬空間Vm_area_struct
Vk空間vm_struct
實體記憶體
Vma->nopage

我們需要實現nopage方法,動態建立對應頁表,而在該方法中核心任務是尋找到vk地址對應的核心邏輯地址[4]。這必然需要我們做以下工作:

a)         找到vmalloc虛擬記憶體對應的核心頁表,並尋找到對應的核心頁表項。

b)        擷取核心頁表項對應的物理頁框指標。

c)        通過頁框得到對應的核心邏輯地址。

[3] 構建使用者也表項,除了使用nopage一次一頁的動態構造,還又一種方法remap_page_range可以一次構造一段記憶體範圍的也表項,但顯然這個方法時針對實體記憶體連續被分配時使用的,而我們vk對應的實體記憶體並非連續,所以這裡使用nopage。

[4] 很多人一定會問,為什麼不直接找到物理地址那,而要找核心邏輯地址呢? 沒錯,我們本意應該是獲得物理地址,但是為了利用核心提供的一些現成的常式,如virt_to_page等(它們都是針對核心邏輯地址而言的),我們不妨轉化成核心邏輯地址來做,別忘了核心邏輯地址與理地址僅僅相差一個位移量。

基本函數
我們執行個體將利用一個虛擬字元驅動程式,驅動負責將一定長的核心虛擬位址(vmalloc分配的)映射到裝置檔案上,以便可以通過訪問檔案內容來達到訪問記憶體的目的。這樣做的最大好處是提高了記憶體訪問速度,並且可以利用檔案系統的介面編程(裝置在Linux中作為特殊檔案處理)訪問記憶體,降低了開發難度。

Map_driver.c就是我們的虛擬字元驅動程式,不用說它要實現檔案動作表(file_operations——字元驅動程式主要做的工作便是實現該結構)中的,為了要完成記憶體映射,除了常規的open/release操作外,必須自己實現mmap操作,該函數將給定的檔案對應到指定的地址空間上,也就是說它將負責把vmalloc分配的核心地址映射到我們的裝置檔案上。

我們下面就談談mmap操作的實現細節:

檔案操作的mmap操作是在使用者進行系統調用mmap[5]時被執行的,而且在調用前核心已經給使用者找到並分配了合適的虛擬記憶體地區vm_area_struct,這個地區將代表檔案內容,所以剩下要做的便是如何把虛擬地區和實體記憶體掛接到一起了,即構造頁表。由於我門前面所說的原因,我們系統中頁表需要動態分配,因此不可使用remap_page_range函數一次分配完成,而必須使用虛擬記憶體地區內建的nopage方法,在現場構造頁表。這樣以來,檔案操作的mmap的方法只要完成“為它得到的虛擬記憶體地區綁定對應的動作表vm_operations”即可。於是主要的構造工作就落在了vm_operations中的nopage方法上了。

Nopage方法中核心內容上面已經提到了是“尋找到vk地址對應的核心邏輯地址”,這個解析核心頁表的工作是需要自己編寫輔助函數vaddr_to_kaddr來完成的,它所作的工作概括來講就是上文提到的a/b/c三條。

有關整個任務執行路徑請看。

 

 

 

[5] 系統調用mmap原形是void *mmap2(void *start, size_t length, int prot, int flags, int fd, off_t pgoff)。
編譯map_driver.c為map_driver.o模組,具體參數見Makefile

載入模組 :insmod map_driver.o

產生對應的裝置檔案

1 在/proc/devices下找到map_driver對應的裝置命和裝置號:grep mapdrv /proc/devices

2 建立裝置檔案mknod mapfile c 254 0 (在我系統裡裝置號為254)

    利用maptest讀取mapfile檔案,將取自核心的資訊(”ok”——我們在核心中在vmalloc分配的空間中填放的資訊)列印到使用者螢幕

 

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.