轉自:http://www.javashuo.com/article/p-mcwulrqo-m.htmlhtml
摘要:本章首先以應用程序開發者的角度審視Linux的進程內存管理,在此基礎上逐步深刻到內核中討論系統物理內存管理和內核內存的使用方法。力求從外到內、水到渠成地引導網友分析Linux的內存管理與使用。在本章最後,咱們給出一個內存映射的實例,幫助網友們理解內核內存管理與用戶內存管理之間的關係,但願你們最終能駕馭Linux內存管理。linux
內存管理一貫是全部操做系統書籍不惜筆墨重點討論的內容,不管市面上或是網上都充斥着大量涉及內存管理的教材和資料。所以,咱們這裏所要寫的Linux內存管理採起拈輕怕重的策略,從理論層面就不去班門弄斧,貽笑大方了。咱們最想作的和可能作到的是從開發者的角度談談對內存管理的理解,最終目的是把咱們在內核開發中使用內存的經驗和對Linux內存管理的認識與你們共享。算法
固然,這其中咱們也會涉及到一些諸如段頁等內存管理的基本理論,但咱們的目的不是爲了強調理論,而是爲了指導理解開發中的實踐,因此僅僅點到爲止,不作深究。編程
遵循「理論來源於實踐」的「教條」,咱們先沒必要一會兒就鑽入內核裏去看系統內存究竟是如何管理,那樣每每會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。因此最好的方式是先從外部(用戶編程範疇)來觀察進程如何使用內存,等到你們對內存的使用有了較直觀的認識後,再深刻到內核中去學習內存如何被管理等理論知識。最後再經過一個實例編程將所講內容融會貫通。小程序
毫無疑問,全部進程(執行的程序)都必須佔用必定數量的內存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數據等等。不過進程對這些內存的管理方式因內存用途不一而不盡相同,有些內存是事先靜態分配和統一回收的,而有些倒是按須要動態分配和回收的。設計模式
對任何一個普通進程來說,它都會涉及到5種不一樣的數據段。稍有編程知識的朋友都能想到這幾個數據段中包含有「程序代碼段」、「程序數據段」、「程序堆棧段」等。不錯,這幾種數據段都在其中,但除了以上幾種數據段以外,進程還另外包含兩種數據段。下面咱們來簡單概括一下進程對應的內存空間中所包含的5種不一樣的數據區。數組
代碼段:代碼段是用來存放可執行文件的操做指令,也就是說是它是可執行程序在內存中的鏡像。代碼段須要防止在運行時被非法修改,因此只准許讀取操做,而不容許寫入(修改)操做——它是不可寫的。緩存
數據段:數據段用來存放可執行文件中已初始化全局變量,換句話說就是存放程序靜態分配[1]的變量和全局變量。服務器
BSS段[2]:BSS段包含了程序中未初始化的全局變量,在內存中 bss段所有置零。數據結構
堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
棧:棧是用戶存放程序臨時建立的局部變量,也就是說咱們函數括弧「{}」中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除此之外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,而且待到調用結束後,函數的返回值也會被存放回棧中。因爲棧的先進先出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
上述幾種內存區域中數據段、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 第1、4G的進程地址空間被人爲的分爲兩個部分——用戶空間與內核空間。用戶空間從0到3G(0xC0000000),內核空間佔據3G到4G。用戶進程一般狀況下只能訪問用戶空間的虛擬地址,不能訪問內核空間虛擬地址。只有用戶進程進行系統調用(表明用戶進程在內核態執行)等時刻能夠訪問到內核空間。
l 第2、用戶空間對應進程,因此每當進程切換,用戶空間就會跟着變化;而內核空間是由內核負責映射,它並不會跟着進程改變,是固定的。內核空間地址有本身對應的頁表(init_mm.pgd),用戶進程各自有不一樣的頁表。
l 第3、每一個進程的用戶空間都是徹底獨立、互不相干的。不信的話,你能夠把上面的程序同時運行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結構確實是以鏈表形式連接,不過爲了方便查找,內核又以紅黑樹(之前的內核使用平衡樹)的形式組織內存區域,以便下降搜索耗時。並存的兩種組織形式,並不是冗餘:鏈表用於須要遍歷所有節點的時候用,而紅黑樹適用於在地址空間中定位特定內存區域的時候。內核爲了內存區域上的各類不一樣操做都能得到高性能,因此同時使用了這兩種數據結構。
下圖反映了進程地址空間的管理模型:
進程的地址空間對應的描述結構是「內存描述符結構」,它表示進程的所有地址空間,——包含了和進程地址空間有關的所有信息,其中固然包含進程的內存區域。
建立進程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體系結構中)大小的頁,從而分配和回收內存的基本單位即是內存頁了。利用分頁管理有助於靈活分配內存地址,由於分配時沒必要要求必須有大塊的連續內存[3],系統能夠東一頁、西一頁的湊出所須要的內存供進程使用。雖然如此,可是實際上系統使用內存時仍是傾向於分配連續的內存塊,由於分配連續內存時,頁表不須要更改,所以能下降TLB的刷新率(頻繁刷新會在很大程度上下降訪問速度)。
鑑於上述需求,內核分配物理頁面時爲了儘可能減小不連續狀況,採用了「夥伴」關係來管理空閒頁面。夥伴關係分配算法你們應該不陌生——幾乎全部操做系統方面的書都會提到,咱們不去詳細說它了,若是不明白能夠參看有關資料。這裏只須要你們明白Linux中空閒頁面的組織和管理利用了夥伴關係,所以空閒頁面分配時也須要遵循夥伴關係,最小單位只能是2的冪倍頁面大小。內核中分配空閒頁面的基本函數是get_free_page/get_free_pages,它們或是分配單頁或是分配指定的頁面(二、四、8…512頁)。
注意:get_free_page是在內核中分配內存,不一樣於malloc在用戶空間中分配,malloc利用堆動態分配,其實是調用brk()系統調用,該調用的做用是擴大或縮小進程堆空間(它會修改進程的brk域)。若是現有的內存區域不夠容納堆空間,則會以頁面大小的倍數爲單位,擴張或收縮對應的內存區域,但brk值並不是以頁面大小爲倍數修改,而是按實際請求修改。所以Malloc在用戶空間分配內存能夠以字節爲單位分配,但內核在內部仍然會是以頁爲單位分配的。
另外,須要說起的是,物理頁在系統中由頁結構struct page描述,系統中全部的頁面都存儲在數組mem_map[]中,能夠經過該數組找到系統中的每一頁(空閒或非空閒)。而其中的空閒頁面則可由上述提到的以夥伴關係組織的空閒頁鏈表(free_area[MAX_ORDER])來索引。
Slab
所謂尺有所長,寸有所短。以頁爲最小單位分配內存對於內核管理系統中的物理內存來講的確比較方便,但內核自身最常使用的內存卻每每是很小(遠遠小於一頁)的內存塊——好比存放文件描述符、進程描述符、虛擬內存區域描述符等行爲所需的內存都不足一頁。這些用來存放描述符的內存相比頁面而言,就比如是麪包屑與麪包。一個整頁中能夠彙集多個這些小塊內存;並且這些小塊內存塊也和麪包屑同樣頻繁地生成/銷燬。
爲了知足內核對這種小內存塊的須要,Linux系統採用了一種被稱爲slab分配器的技術。Slab分配器的實現至關複雜,但原理不難,其核心思想就是「存儲池[4]」的運用。內存片斷(小塊內存)被看做對象,當被使用完後,並不直接釋放而是被緩存到「存儲池」裏,留作下次使用,這無疑避免了頻繁建立與銷燬對象所帶來的額外負載。
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準備的)。
內核非連續內存分配(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); }
內存映射(mmap)是Linux操做系統的一個很大特點,它能夠將系統內存映射到一個文件(設備)上,以即可以經過訪問文件內容來達到訪問內存的目的。這樣作的最大好處是提升了內存訪問速度,而且能夠利用文件系統的接口編程(設備在Linux中做爲特殊文件處理)訪問內存,下降了開發難度。許多設備驅動程序即是利用內存映射功能將用戶空間的一段地址關聯到設備內存上,不管什麼時候,只要內存在分配的地址範圍內進行讀寫,實際上就是對設備內存的訪問。同時對設備文件的訪問也等同於對內存區域的訪問,也就是說,經過文件操做接口能夠訪問內存。Linux中的X服務器就是一個利用內存映射達到直接高速訪問視頻卡內存的例子。
熟悉文件操做的朋友必定會知道file_operations結構中有mmap方法,在用戶執行mmap系統調用時,便會調用該方法來經過文件訪問內存——不過在調用文件系統mmap方法前,內核還須要處理分配內存區域(vma_struct)、創建頁表等工做。對於具體映射細節不做介紹了,須要強調的是,創建頁表能夠採用remap_page_range方法一次創建起全部映射區的頁表,或利用vma_struct的nopage方法在缺頁時現場一頁一頁的創建頁表。第一種方法相比第二種方法簡單方便、速度快, 可是靈活性不高。一次調用全部頁表便定型了,不適用於那些須要現場創建頁表的場合——好比映射區須要擴展或下面咱們例子中的狀況。
咱們這裏的實例但願利用內存映射,將系統內核中的一部分虛擬內存映射到用戶空間,以供應用程序讀取——你可利用它進行內核空間到用戶空間的大規模信息傳輸。所以咱們將試圖寫一個虛擬字符設備驅動程序,經過它將系統內核空間映射到用戶空間——將內核虛擬內存映射到用戶虛擬地址。從上一節已經看到Linux內核空間中包含兩種虛擬地址:一種是物理和邏輯都連續的物理內存映射虛擬地址;另外一種是邏輯連續但非物理連續的vmalloc分配的內存虛擬地址。咱們的例子程序將演示把vmalloc分配的內核虛擬地址映射到用戶地址空間的全過程。
程序裏主要應解決兩個問題:
第一是如何將vmalloc分配的內核虛擬內存正確地轉化成物理地址?
由於內存映射先要得到被映射的物理地址,而後才能將其映射到要求的用戶虛擬地址上。咱們已經看到內核物理內存映射區域中的地址能夠被內核函數virt_to_phys轉換成實際的物理內存地址,但對於vmalloc分配的內核虛擬地址沒法直接轉化成物理地址,因此咱們必須對這部分虛擬內存格外「照顧」——先將其轉化成內核物理內存映射區域中的地址,而後在用virt_to_phys變爲物理地址。
轉化工做須要進行以下步驟:
a) 找到vmalloc虛擬內存對應的頁表,並尋找到對應的頁表項。
b) 獲取頁表項對應的頁面指針
c) 經過頁面獲得對應的內核物理內存映射區域地址。
以下圖所示:
第二是當訪問vmalloc分配區時,若是發現虛擬內存還沒有被映射到物理頁,則須要處理「缺頁異常」。所以須要咱們實現內存區域中的nopaga操做,以能返回被映射的物理頁面指針,在咱們的實例中就是返回上面過程當中的內核物理內存映射區域中的地址。因爲vmalloc分配的虛擬地址與物理地址的對應關係並不是分配時就可肯定,必須在缺頁現場創建頁表,所以這裏不能使用remap_page_range方法,只能用vma的nopage方法一頁一頁的創建。
程序組成
map_driver.c,它是以模塊形式加載的虛擬字符驅動程序。該驅動負責將必定長的內核虛擬地址(vmalloc分配的)映射到設備文件上。其中主要的函數有——vaddress_to_kaddress()負責對vmalloc分配的地址進行頁表解析,以找到對應的內核物理映射地址(kmalloc分配的地址);map_nopage()負責在進程訪問一個當前並不存在的VMA頁時,尋找該地址對應的物理頁,並返回該頁的指針。
test.c 它利用上述驅動模塊對應的設備文件在用戶空間讀取讀取內核內存。結果能夠看到內核虛擬地址的內容(ok!),被顯示在了屏幕上。
執行步驟
編譯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文件,將取自內核的信息打印到屏幕上。