摘要:本章首先以應用程序開發(fā)者的角度審視Linux的進程內存管理,在此基礎上逐步深入到內核中討論系統(tǒng)物理內存管理和內核內存地使用方法。力求從外自內、水到渠成地引導網友分析Linux地內存管理與使用。在本章最后我們給出一個內存映射地實例,幫助網友們理解內核內存管理與用戶內存管理之間地關系,希望大家最終能駕馭Linux內存管理。
內存管理一向是所有操作系統(tǒng)書籍不惜筆墨重點討論的內容,無論市面上或是網上都充斥著大量涉及內存管理的教材和資料。因此我們這里所要寫的Linux內存管理采取必重就輕的策略,從理論層面就不去板門弄斧,貽笑大方了。我們最想做的和可能做到的是以開發(fā)者的角度談談對內存管理的理解,最終目的是把我們在內核開發(fā)中使用內存的經驗和對Linux內存管理的認識與大家共享。
當然這其中我們也會設計一些諸如段頁等內存管理的基本理論,但我們目的不是為了強調理論,而是為了指導理解開發(fā)中的實踐,所以僅僅點到為止,不做深究。
遵循“理論來源于實踐”的“教條”,我們先不必一下子就鉆入內核里去看系統(tǒng)內存到底是如何管理,那樣往往會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。所以最好的方式是先從外部(用戶編程范疇)來觀察進程如何使用內存,等到對大家內存使用有了較直觀的認識后,再深入到內核中去學習內存如何被管理等理論知識。最后再通過一個實例編程將所講內容融會貫通。
毫無疑問所有進程(執(zhí)行的程序)都必須占用一定數(shù)量的內存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數(shù)據(jù)等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態(tài)分配和統(tǒng)一回收的,而有些卻是按需要動態(tài)分配和回收的。
對任何一個普通進程來講,它都會涉及到5種不同的數(shù)據(jù)段。稍有編程知識的朋友都該能想到這幾個數(shù)據(jù)段種包含有“程序代碼段”、“程序數(shù)據(jù)段”、“程序堆棧段”等。不錯,這幾種數(shù)據(jù)段都在其中,但除了以上幾種數(shù)據(jù)段之外,進程還另外包含兩種數(shù)據(jù)段。下面我們來簡單歸納一下進程對應的內存空間中所包含的5種不同的數(shù)據(jù)區(qū)。
代碼段:代碼段是用來存放可執(zhí)行文件的操作指令,也就是說是它是可執(zhí)行程序在內存種的鏡像。代碼段需要防止在運行時被非法修改,所以只準許讀取操作,而不允許寫入(修改)操作——它是不可寫的。
數(shù)據(jù)段:數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,換句話說就是存放程序靜態(tài)分配[1]的變量和全局變量。
BSS段[2]:BSS段包含了程序中未初始化全局變量,在內存中 bss段全部置零。
堆(heap):堆是用于存放進程運行中被動態(tài)分配的內存段,它大小并不固定,可動態(tài)擴張或縮減。當進程調用malloc等函數(shù)分配內存時,新分配的內存就被動態(tài)添加到堆上(堆被擴張);當利用free等函數(shù)釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
棧:棧是用戶存放程序臨時創(chuàng)建的局部變量,也就是說我們函數(shù)括弧“{}”中定義的變量(但不包括static聲明的變量,static意味這在數(shù)據(jù)段中存放變量)。除此以外在函數(shù)被調用時,其參數(shù)也會被壓入發(fā)起調用的進程棧中,并且待到調用結束后,函數(shù)的返回值也回被存放回棧中。由于棧的先進先出特點,所以棧特別方便用來保存/恢復調用現(xiàn)場。從這個意義上將我們可以把堆??闯梢粋€臨時數(shù)據(jù)寄存、交換的內存區(qū)。
上述幾種內存區(qū)域中數(shù)據(jù)段、BSS和堆通常是被連續(xù)存儲的——內存位置上是連續(xù)的,而代碼段和棧往往會被獨立存放。有趣的是堆和棧兩個區(qū)域關系很“曖昧”,他們一個向下“長”(i386體系結構中棧向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程序計算一下),絕少有機會能碰到一起。
下圖簡要描述了進程內存區(qū)域的分布:
數(shù)據(jù)段 BSS 代碼段 堆 棧
“事實勝于雄辯”,我們用一個小例子(原形取自《User-Level Memory Management》)來展示上面所講的各種內存區(qū)的差別與位置。
#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):0x
____________________________
BSS Location:
Address of bss_var:0x8049864
____________________________
Heap Location:
Initial end of heap:0x8049868
New end of heap:0x
利用size命令也可以看到程序的各段大小,比如執(zhí)行size example會得到
text data bss dec hex filename
1654 280 8 1942 796 example
但這些數(shù)據(jù)是程序編譯的靜態(tài)統(tǒng)計,而上面顯示的是進程運行時動態(tài)值,但兩者是對應的。
從前面的例子,我們對進程使用的邏輯內存分布已經先睹為快。這部分我們就繼續(xù)進入操作系統(tǒng)內核看看進程對內存具體是如何進行分配和管理的。
從用戶向內核看,所使用的內存表象形式會依次經歷“邏輯地址”——“線形地址”——“物理地址”幾種形式(關于幾種地址的解釋在前面已經講述了)。邏輯地址經段機制轉化成線性地址;線性地址又經過頁機制轉化為物理地址。(但是我們要知道Linux系統(tǒng)雖然保留了段機制,但是將所有程序的段地址都定死為0
1. 進程空間地址如何管理?
2. 進程地址如何映射到物理內存?
3. 物理內存如何被管理?
以及由上述問題引發(fā)的一些子問題。如系統(tǒng)虛擬地址分布;內存分配接口;連續(xù)內存分配與非連續(xù)內存分配等。
Linux操作系統(tǒng)采用虛擬內存管理技術,使得每個進程都有各自互不干涉的進程地址空間。該空間是塊大小為
在討論進程空間細節(jié)前,請大家這里先要澄清下面幾個問題。
l 第一、
l 第二、用戶空間對應進程,所以每當進程切換,用戶空間就會跟著變化;而內核空間是由內核負責映射,它并不會跟著進程改變,是固定的。內核空間地址有自己對應的頁表(init_mm.pgd),用戶進程各自有不同的頁表(。
l 第三、每個進程的用戶空間都是完全獨立、互不相干的。不信的話,你可以把上面的程序同時運行10次(當然為了同時運行,讓它們在返回前一同睡眠100秒吧),你會看到10個進程占用的線性地址一模一樣。
進程內存管理的對象是進程線性地址空間上的內存鏡像,這些內存鏡像其實就是進程使用的虛擬內存區(qū)域(memory region)。進程虛擬空間是個32或64位的“平坦”(獨立的連續(xù)區(qū)間)地址空間(空間的具體大小取決于體系結構)。要統(tǒng)一管理這么大的平坦空間可絕非易事,為了方便管理,虛擬空間被化分為許多大小可變的(但必須是4096的倍數(shù))內存區(qū)域,這些區(qū)域在進程線性地址中像停車位一樣有序排列。這些區(qū)域的劃分原則是“將訪問屬性一致的地址空間存放在一起”,所謂訪問屬性在這里無非指的是“可讀、可寫、可執(zhí)行等”。
如果你要查看某個進程占用的內存區(qū)域,可以使用命令cat /proc/<pid>/maps獲得(pid是進程號,你可以運行上面我們給出的例子——./example &;pid便會打印到屏幕),你可以發(fā)現(xiàn)很多類似于下面的數(shù)字信息。
由于程序example使用了動態(tài)庫,所以除了example本身使用的的內存區(qū)域外,還會包含那些動態(tài)庫使用的內存區(qū)域(區(qū)域順序是:代碼段、數(shù)據(jù)段、bss段)。
我們下面只抽出和example有關的信息,除了前兩行代表的代碼段和數(shù)據(jù)段外,最后一行是進程使用的??臻g。
-------------------------------------------------------------------------------
08048000 - 08049000 r-xp 00000000
08049000 -
……………
bfffe000 - c0000000 rwxp ffff000
----------------------------------------------------------------------------------------------------------------------
每行數(shù)據(jù)格式如下:
(內存區(qū)域)開始-結束訪問權限 偏移 主設備號:次設備號 i節(jié)點 文件。
注意,你一定會發(fā)現(xiàn)進程空間只包含三個內存區(qū)域,似乎沒有上面所提到的堆、bss等,其實并非如此,程序內存段和進程地址空間中的內存區(qū)域是種模糊對應,也就是說,堆、bss、數(shù)據(jù)段(初始化過的)都在進程空間種由數(shù)據(jù)段內存區(qū)域表示。
在Linux內核中對應進程內存區(qū)域的數(shù)據(jù)結構是: vm_area_struct, 內核將每個內存區(qū)域作為一個單獨的內存對象管理,相應的操作也都一致。采用面向對象方法使VMA結構體可以代表多種類型的內存區(qū)域--比如內存映射文件或進程的用戶空間棧等,對這些區(qū)域的操作也都不盡相同。
vm_area_strcut結構比較復雜,關于它的詳細結構請參閱相關資料。我們這里只對它的組織方法做一點補充說明。vm_area_struct是描述進程地址空間的基本管理單元,對于一個進程來說往往需要多個內存區(qū)域來描述它的虛擬空間,如何關聯(lián)這些不同的內存區(qū)域呢?大家可能都會想到使用鏈表,的確vm_area_struct結構確實是已鏈表形式鏈接,不過位了方便查找,內核又以紅黑樹(以前的內核使用平衡樹)的形式組織內存區(qū)域,以便降低搜索耗時。并存兩種組織形式,并非冗余:鏈表用于需要遍歷全部節(jié)點的時候用,而紅黑樹適用于在地址空間中定位特定內存區(qū)域的時候。內核為了內存區(qū)域上的各種不同操作都能獲得高性能,所以同時使用了這兩種數(shù)據(jù)結構。
下圖反映了進程地址空間的管理模型:
mmap 進程內存描述符 Vm_area_struct 進程虛擬地址
進程的地址空間對應的描述結構是“內存描述符結構”,它表示進程的全部地址空間,——包含了和進程地址空間有關的全部信息,其中當然包含進程的內存區(qū)域。
創(chuàng)建進程fork()、程序載入execve()、映射文件mmap()、動態(tài)內存分配malloc()/brk()等進程相關操作都需要分配內存給進程。不過這時進程申請和獲得的還不是實際內存,而是虛擬內存,準確的說是“內存區(qū)域”。進程對內存區(qū)域的分配最終多會歸結到do_mmap()函數(shù)上來(brk調用被單獨以系統(tǒng)調用實現(xiàn),不用do_mmap()),
內核使用do_mmap()函數(shù)創(chuàng)建一個新的線性地址區(qū)間。但是說該函數(shù)創(chuàng)建了一個新VMA并不非常準確,因為如果創(chuàng)建的地址區(qū)間和一個已經存在的地址區(qū)間相鄰,并且它們具有相同的訪問權限的話,那么兩個區(qū)間將合并為一個。如果不能合并,那么就確實需要創(chuàng)建一個新的VMA了。但無論哪種情況, do_mmap()函數(shù)都會將一個地址區(qū)間加入到進程的地址空間中--無論是擴展已存在的內存區(qū)域還是創(chuàng)建一個新的區(qū)域。
同樣釋放一個內存區(qū)域使用函數(shù)do_ummap(),它會銷毀對應的內存區(qū)域。
從上面已經看到進程所能直接操作的地址都為虛擬地址。當進程需要內存時,從內核獲得的僅僅時虛擬的內存區(qū)域,而不是實際的物理地址,進程并沒有獲得物理內存(物理頁框——頁的概念請大家參與硬件基礎一章),獲得的僅僅是對一個新的線性地址區(qū)間的使用權。實際的物理內存只有當進程真的去訪問新獲取的虛擬地址時,才會由“請頁機制”產生“缺頁”異常,從而進入分配實際頁框的例程。
該異常是虛擬內存機制賴以存在的基本保證——它會告訴內核去真正為進程分配物理頁,并建立對應的頁表,這之后虛擬地址才實實在在映射到了系統(tǒng)物理內存上。(當然如果頁被換出到磁盤,也會產生缺頁異常,不過這時不用再建立頁表了)
這種請頁機制把頁框的分配推遲到不能再推遲為止,并不急于把所有的事情都一次做完(這中思想由點想涉及模式中的代理模式(proxy))。之所以能這么做是利用了內存訪問的“局部性原理”,請頁帶來的好處是節(jié)約了空閑內存,提高了系統(tǒng)吞吐。要想更清楚的了解請頁,可以看看《深入理解linux內核》一書。
這里我們需要說明在內存區(qū)域結構上的nopage操作,該操作是當發(fā)生訪問的進程虛擬內存而發(fā)現(xiàn)并未真正分配頁框時,該方法變被調用來分配實際的物理頁,并為該頁建立頁表項。在最后的例子中我們會演示如何使用該方法。
雖然應用程序操作的對象是映射到物理內存之上的虛擬內存,但是處理器直接操作的卻是物理內存。所以當用程序訪問一個虛擬地址時,首先必須將虛擬地址轉化成物理地址,然后處理器才能解析地址訪問請求。地址的轉換工作需要通過查詢頁表才能完成,概括的講,地址轉換需要將虛擬地址分段,使每段虛地址都作為一個索引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。
每個進程都有自己的頁表。進程描述符號的pgd域指向的就是進程的頁全局目錄。席面我們借用《linux設備驅動程序》中的一幅圖大致看看進程地址空間到物理頁之間的轉換關系。
上面的過程說起簡單,做起難呀。因為在虛擬地址映射到頁之前必須先分配物理頁——也就是說必須先從內核獲取空閑頁,并建立頁表。下面我們介紹一下內核管理物理內存的機制。
Linux內核管理物理內存是通過分頁機制實現(xiàn)的,它將整個內存劃分成無數(shù)4k(在i386體系結構中)大小頁,從而分配和回收內存的基本單位便是內存頁了。利用分頁管理有助于靈活分配內存地址,因為分配時不必要求必須有大塊的連續(xù)內存[3],系統(tǒng)可以東一頁、西一頁的湊出所需要的內存供進程使用。雖然如此,但是實際上系統(tǒng)使用內存還是傾向于分配連續(xù)的內存塊,因為分配連續(xù)內存時,頁表不需要更改,因此能降低TLB的刷新率(頻繁刷新會很大增加訪問速度)。
鑒于上述需求,內核分配物理頁為了盡量減少不連續(xù)情況,采用了“伙伴”關系來管理空閑頁框。伙伴關系分配算法大家不應陌生——幾乎所有操作系統(tǒng)書都會提到,我們不去詳細說它了,如果不明白可以參看有關資料。這里只需要大家明白Linux中空閑頁面的組織和管理利用了伙伴關系,因此空閑頁面分配時也需要遵循伙伴關系,最小單位只能是2的冪倍頁面大小。內核中分配空閑頁框的基本函數(shù)是get_free_page/get_free_pages,它們或是分配單頁或是分配指定的頁框(2、4、8…512頁)。
注意:get_free_page是在內核中分配內存,不同于malloc在用戶空間中分配,malloc利用堆動態(tài)分配,實際上是調用brk()系統(tǒng)調用,該調用的作用是擴大或縮小進程堆空間(它會修改進程的brk域)。如果現(xiàn)有的內存區(qū)域不夠容納堆空間,則會以頁面大小的倍數(shù)位單位,擴張或收縮對應的內存區(qū)域,但brk值并非以頁面大小為倍數(shù)修改,而是按實際請求修改。因此Malloc在用戶空間分配內存可以以字節(jié)為單位分配,但內核在內部仍然會是以頁為單位分配的。
另外需要提及的是,物理頁在系統(tǒng)中由頁框結構struct paga描述,系統(tǒng)中所有的頁框存儲在數(shù)組mem_map[]中,可以通過該數(shù)組找到系統(tǒng)中的每一頁(空閑或非空閑)。而其中的空閑頁框則可由上述提到的以伙伴關系組織的空閑頁鏈表(free_area[MAX_ORDER])索引。
空閑頁框 APP 內存區(qū)域 vm_area_structs malloc、fork、excute、mmap brk/do_map get_free_page(s) 用戶空間 內核空間 進程虛擬地址空間 系統(tǒng)調用 進程頁表 請頁異常
Slab
所謂尺有所長,寸有所短。以頁為最小單位分配內存對于內核管理系統(tǒng)物理內存來說的確比較方便,但內核自身最常使用的內存卻往往是很?。ㄟh遠小于一頁)的內存塊——比如存放文件描述符、進程描述符、虛擬內存區(qū)域描述符等行為所需的內存都不足一頁。這些用來存放描述符的內存相比頁面而言,就好比是面包屑與面包。一個整頁中可以聚集多個這種這些小塊內存;而且這些小塊內存塊也和面包屑一樣頻繁地生成/銷毀。
為了滿足內核對這種小內存塊的需要,Linux系統(tǒng)采用了一種被稱為slab分配器的技術。Slab分配器的實現(xiàn)相當復雜,但原理不難,其核心思想就是“存儲池[4]”的運用。內存片段(小塊內存)被看作對象,當被使用完后,并不直接釋放而是被緩存到“存儲池”里,留做下次使用,這無疑避免了頻繁創(chuàng)建與銷毀對象所帶來的額外負載。
Slab技術不但避免了內存內部分片(下文將解釋)帶來的不便(引入Slab分配器的主要目的是為了減少對伙伴系統(tǒng)分配算法的調用次數(shù)——頻繁分配和回收必然會導致內存碎片——難以找到大塊連續(xù)的可用內存),而且可以很好利用硬件緩存提高訪問速度。
Slab并非是脫離伙伴關系而獨立存在的一種內存分配方式,slab仍然是建立在頁面基礎之上,換句話說,Slab將頁面(來自于伙伴關系管理的空閑頁框鏈)撕碎成眾多小內存塊以供分配,slab中的對象分配和銷毀使用kmem_cache_alloc與kmem_cache_free。
Kmalloc
Slab分配器不僅僅只用來存放內核專用的結構體,它還被用來處理內核對小塊內存的請求。當然鑒于Slab分配器的特點,一般來說內核程序中對小于一頁的小塊內存的求情才通過Slab分配器提供的接口Kmalloc來完成(雖然它可分配32 到131072字節(jié)的內存)。從內核內存分配角度講kmalloc可被看成是get_free_page(s)的一個有效補充,內存分配粒度更靈活了。
有興趣的話可以到/proc/slabinfo中找到內核執(zhí)行現(xiàn)場使用的各種slab信息統(tǒng)計,其中你會看到系統(tǒng)中所有slab的使用信息。從信息中可以看到系統(tǒng)中除了專用結構體使用的slab外,還存在大量為Kmalloc而準備的Slab(其中有些為dma準備的)。
內核非連續(xù)內存分配(Vmalloc)
伙伴關系也好、slab技術也好,從內存管理理論角度而言目的基本是一致的,它們都是為了防止“分片”,不過分片又分為外部分片和內部分片之說,所謂內部分片是說系統(tǒng)為了滿足一小段內存區(qū)(連續(xù))的需要,不得不分配了一大區(qū)域連續(xù)內存給它,從而造成了空間浪費;外部分片是指系統(tǒng)雖有足夠的內存,但卻是分散的碎片,無法滿足對大塊“連續(xù)內存”的需求。無論何種分片都是系統(tǒng)有效利用內存的障礙。slab分配器使得含與一個頁面內眾多小塊內存可獨立被分配使用,避免了內部分片,節(jié)約了空閑內存?;锇殛P系把內存塊按大小分組管理,一定程度上減輕了外部分片的危害,因為頁框分配不在盲目,而是按照大小依次有序進行,不過伙伴關系只是減輕了外部分片,但并未徹底消除。你自己筆畫一下多次分配頁框后,空閑內存的剩余情況吧。
所以避免外部分片的最終思路還是落到了如何利用不連續(xù)的內存塊組合成“看起來很大的內存塊”——這里的情況很類似于用戶空間分配虛擬內存,內存邏輯上連續(xù),其實影射到并不一定連續(xù)的物理內存上。Linux內核借用了這個技術,允許內核程序在內核地址空間中分配虛擬地址,同樣也利用頁表(內核頁表)將虛擬地址影射到分散的內存頁上。以此完美地解決了內核內存使用中的外部分片問題。內核提供vmalloc函數(shù)分配內核虛擬內存,該函數(shù)不同于kmalloc,它可以分配較Kmalloc大得多的內存空間(可遠大于128K,但必須是頁大小的倍數(shù)),但相比Kmalloc來說Vmalloc需要對內核虛擬地址進行重影射,必須更新內核頁表,因此分配效率上要低一些(用空間換時間)
與用戶進程相似內核也有一個名為init_mm的mm_strcut結構來描述內核地址空間,其中頁表項pdg=swapper_pg_dir包含了系統(tǒng)內核空間(
空閑頁框 APP 內存區(qū)域 vm_area_structs malloc、fork、excute、mmap brk/do_map get_free_page(s) 用戶空間 內核空間 進程虛擬地址空間 系統(tǒng)調用 進程頁表 請頁異常 內核程序 物理內存影射區(qū) Vmalloc分配區(qū) slab get_free_page(s) 內核頁表 get_free_page(s) 請頁異常
vmalloc分配的內核虛擬內存與kmalloc/get_free_page分配的內核虛擬內存位于不同的區(qū)間,不會重疊。因為內核虛擬空間被分區(qū)管理,各司其職。進程空間地址分布從0到
進程地址空間 物理內存映射區(qū) 0 內核虛擬空間 Vmalloc_start Vmalloc_end
上圖是內存分布的模糊輪廓
由get_free_page或Kmalloc函數(shù)所分配的連續(xù)內存都陷于物理映射區(qū)域,所以它們返回的內核虛擬地址和實際物理地址僅僅是相差一個偏移量(PAGE_OFFSET),你可以很方便的將其轉化為物理內存地址,同時內核也提供了virt_to_phys()函數(shù)將內核虛擬空間中的物理影射區(qū)地址轉化為物理地址。要知道,物理內存映射區(qū)中的地址與內核頁表是有序對應,系統(tǒng)中的每個物理頁框都可以找到它對應的內核虛擬地址(在物理內存映射區(qū)中的)。
而vmalloc分配的地址則限于vmalloc_start與vmalloc_end之間。每一塊vmalloc分配的內核虛擬內存都對應一個vm_struct結構體(可別和vm_area_struct搞混,那可是進程虛擬內存區(qū)域的結構),不同的內核虛擬地址被4k打大小空閑區(qū)的間隔,以防止越界——見下圖)。與進程虛擬地址的特性一樣,這些虛擬地址可與物理內存沒有簡單的位移關系,必須通過內核頁表才可轉換為物理地址或物理頁。它們有可能尚未被映射,在發(fā)生缺頁時才真正分配物理頁框。
這里給出一個小程序幫助大家認請上面幾種分配函數(shù)所對應的區(qū)域。
#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);
}
我們希望能通過訪問用戶空間的內存達到讀取內核數(shù)據(jù)的目的,這樣便可進行內核空間到用戶空間的大規(guī)模信息傳輸。
具體的講,我們要利用內存映射功能,將系統(tǒng)內核中的一部分虛擬內存映射到用戶空間,從而使得用戶空間地址等同與被映射的內核內存地址。
因此我們將試圖寫一個虛擬字符設備驅動程序,通過它將系統(tǒng)內核空間映射到用戶空間——將內核虛擬內存映射到用戶虛擬地址。當然映射地址時少不了定位內核空間對應的物理地址,并且還要建立新的用戶頁表項,以便用戶進程尋址時能找到對應的物理內存。
從中應該看出,需要我完成既定目標,我們需要獲得:被映射內核空間物理地址 和 建立對應的用戶進程頁表。
在內核空間中主要存在kmalloc分配的物理連續(xù)空間和vmalloc分配的非物理連續(xù)空間。kmalloc分配的空間往往被稱為內核邏輯地址,由于它是連續(xù)分配(直接處理物理頁框),而且分配首地址一定,所以其分配的內核虛擬地址對應的實際物理地址很容易獲得:內核虛擬地址—PAGE_OFFSET(0xC0000000)(內核有對應例程virt_to_phys)即等于物理地址,而且其對應的頁表屬于內核頁表(swapper_pg_dir)——在系統(tǒng)初始化時就以建立,因此省去了建立頁表的工作。
而vmalloc分配的空間被稱為內核虛擬地址,它的問題相對要復雜些,這是因為其分配的內核虛擬內存空間并非直接操作頁框,而是分配的是vm_struct結構。該結構邏輯上連續(xù)但對應的物理內存并非連續(xù),也就是說它vamlloc分配的內核空間地址所對應的物理地址并非可通過簡單線性運算獲得。從這個意義上講,它的物理地址在分配前是不確定的,因此雖然vmalloc分配的空間與kmalloc一樣都是由內核頁表來映射的,但vmalloc分配內核虛擬地址時必須更新內核頁表。
注釋:vmalloc分配的內核虛擬內存與kmalloc/get_free_page分配的內核邏輯內存位于不同的區(qū)間,不會重疊。因為內核空間被分區(qū)管理,各司其職。進程空間地址分布從0到
另一個需要澄清的是,vmalloc分配的內核空間,其結構是vm_area,可千萬別與用戶空間malloc分配的vm_area_struct結構混淆。前者由內核頁表映射,而后者則由用戶頁表映射。
進程地址空間 物理內存映射區(qū)kmalloc分配 Vmalloc 分配區(qū) 0 內核虛擬空間 Vmalloc_start Vmalloc_end
上圖是內存分布的模糊輪廓
為了近可能豐富我們的例子程序的場景,我們選擇映射vmalloc分配的內核虛擬空間(下面我們簡稱為vk地址)到用戶空間。
要知道用戶進程操作的是虛擬內存區(qū)域vm_area_struct,我們此刻需要將用戶vma區(qū)間利用用戶頁表映射到vk對應的物理內存上去(如下圖所示)。這里主要工作便是建立用戶也表項完成映射工作,而這個工作完全落在了vma->nopage[5]操作上,該方法會幫助我們在發(fā)生“缺頁”時,動態(tài)構造映射所需物理內存的頁表項。
用戶虛擬空間Vm_area_struct Vk空間vm_struct 物理內存 Vma->nopage
我們需要實現(xiàn)nopage方法,動態(tài)建立對應頁表,而在該方法中核心任務是尋找到vk地址對應的內核邏輯地址[6]。這必然需要我們做以下工作:
a) 找到vmalloc虛擬內存對應的內核頁表,并尋找到對應的內核頁表項。
b) 獲取內核頁表項對應的物理頁框指針。
c) 通過頁框得到對應的內核邏輯地址。
我們實例將利用一個虛擬字符驅動程序,驅動負責將一定長的內核虛擬地址(vmalloc分配的)映射到設備文件上,以便可以通過訪問文件內容來達到訪問內存的目的。這樣做的最大好處是提高了內存訪問速度,并且可以利用文件系統(tǒng)的接口編程(設備在Linux中作為特殊文件處理)訪問內存,降低了開發(fā)難度。
Map_driver.c就是我們的虛擬字符驅動程序,不用說它要實現(xiàn)文件操作表(file_operations——字符驅動程序主要做的工作便是實現(xiàn)該結構)中的,為了要完成內存映射,除了常規(guī)的open/release操作外,必須自己實現(xiàn)mmap操作,該函數(shù)將給定的文件映射到指定的地址空間上,也就是說它將負責把vmalloc分配的內核地址映射到我們的設備文件上。
我們下面就談談mmap操作的實現(xiàn)細節(jié):
文件操作的mmap操作是在用戶進行系統(tǒng)調用mmap[7]時被執(zhí)行的,而且在調用前內核已經給用戶找到并分配了合適的虛擬內存區(qū)域vm_area_struct,這個區(qū)域將代表文件內容,所以剩下要做的便是如何把虛擬區(qū)域和物理內存掛接到一起了,即構造頁表。由于我門前面所說的原因,我們系統(tǒng)中頁表需要動態(tài)分配,因此不可使用remap_page_range函數(shù)一次分配完成,而必須使用虛擬內存區(qū)域自帶的nopage方法,在現(xiàn)場構造頁表。這樣以來,文件操作的mmap的方法只要完成“為它得到的虛擬內存區(qū)域綁定對應的操作表vm_operations”即可。于是主要的構造工作就落在了vm_operations中的nopage方法上了。
Nopage方法中核心內容上面已經提到了是“尋找到vk地址對應的內核邏輯地址”,這個解析內核頁表的工作是需要自己編寫輔助函數(shù)vaddr_to_kaddr來完成的,它所作的工作概括來講就是上文提到的a\b\c三條。
有關整個任務執(zhí)行路徑請看下圖。
獲vm_area對應的內核邏輯地址 (page_address) 獲得vm_area對應內核頁表項指針 (pte_offset) 得到內核邏輯地址對應的進程頁框 (virt_to_page) 獲得vm_area對應的內核邏輯地址(vaddr_to_kaddr) File->mma (mapdrv_mmap) mmap系統(tǒng)調用 Vma->nopage(map_nopage) 獲得vm_area對應的內核頁表項 (pgd_offset_k pmd_offset pte_offset)
編譯map_driver.c為map_driver.o模塊,具體參數(shù)見Makefile
加載模塊 :insmod map_driver.o
生成對應的設備文件
1 在/proc/devices下找到map_driver對應的設備命和設備號:grep mapdrv /proc/devices
2 建立設備文件mknod mapfile c 254 0 (在我系統(tǒng)里設備號為254)
利用maptest讀取mapfile文件,將取自內核的信息(”ok”——我們在內核中在vmalloc分配的空間中填放的信息)打印到用戶屏幕。
全部程序下載 mmap.tar (感謝Martin Frey,該程序主體出自他的靈感)