• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-20  評論-0  文章-0  trackbacks-0

            本章首先以應用程序開發(fā)者的角度審視Linux的進程內(nèi)存管理,在此基礎上逐步深入到內(nèi)核中討論系統(tǒng)物理內(nèi)存管理和內(nèi)核內(nèi)存的使用方法。力求從外到內(nèi)、水到渠成地引導網(wǎng)友分析Linux的內(nèi)存管理與使用。在本章最后,我們給出一個內(nèi)存映射的實例,幫助網(wǎng)友們理解內(nèi)核內(nèi)存管理與用戶內(nèi)存管理之間的關系,希望大家最終能駕馭Linux內(nèi)存管理。

            前言

            內(nèi)存管理一向是所有操作系統(tǒng)書籍不惜筆墨重點討論的內(nèi)容,無論市面上或是網(wǎng)上都充斥著大量涉及內(nèi)存管理的教材和資料。因此,我們這里所要寫的Linux內(nèi)存管理采取避重就輕的策略,從理論層面就不去班門弄斧,貽笑大方了。我們最想做的和可能做到的是從開發(fā)者的角度談談對內(nèi)存管理的理解,最終目的是把我們在內(nèi)核開發(fā)中使用內(nèi)存的經(jīng)驗和對Linux內(nèi)存管理的認識與大家共享。

            當然,這其中我們也會涉及到一些諸如段頁等內(nèi)存管理的基本理論,但我們的目的不是為了強調(diào)理論,而是為了指導理解開發(fā)中的實踐,所以僅僅點到為止,不做深究。

            遵循“理論來源于實踐”的“教條”,我們先不必一下子就鉆入內(nèi)核里去看系統(tǒng)內(nèi) 存到底是如何管理,那樣往往會讓你陷入似懂非懂的窘境(我當年就犯了這個錯誤!)。所以最好的方式是先從外部(用戶編程范疇)來觀察進程如何使用內(nèi)存,等 到大家對內(nèi)存的使用有了較直觀的認識后,再深入到內(nèi)核中去學習內(nèi)存如何被管理等理論知識。最后再通過一個實例編程將所講內(nèi)容融會貫通。

            進程與內(nèi)存

            進程如何使用內(nèi)存?

            毫無疑問,所有進程(執(zhí)行的程序)都必須占用一定數(shù)量的內(nèi)存,它或是用來存放從磁盤載入的程序代碼,或是存放取自用戶輸入的數(shù)據(jù)等等。不過進程對這些內(nèi)存的管理方式因內(nèi)存用途不一而不盡相同,有些內(nèi)存是事先靜態(tài)分配和統(tǒng)一回收的,而有些卻是按需要動態(tài)分配和回收的。

            對任何一個普通進程來講,它都會涉及到5種不同的數(shù)據(jù)段。稍有編程知識的朋友都能想到這幾個數(shù)據(jù)段中包含有“程序代碼段”、“程序數(shù)據(jù)段”、“程序堆棧段”等。不錯,這幾種數(shù)據(jù)段都在其中,但除了以上幾種數(shù)據(jù)段之外,進程還另外包含兩種數(shù)據(jù)段。下面我們來簡單歸納一下進程對應的內(nèi)存空間中所包含的5種不同的數(shù)據(jù)區(qū)。

            代碼段:代碼段是用來存放可執(zhí)行文件的操作指令,也就是說是它是可執(zhí)行程序在內(nèi)存中的鏡像。代碼段需要防止在運行時被非法修改,所以只準許讀取操作,而不允許寫入(修改)操作——它是不可寫的。

            數(shù)據(jù)段:數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,換句話說就是存放程序靜態(tài)分配[1]的變量和全局變量。

            BSS[2]BSS段包含了程序中未初始化的全局變量,在內(nèi)存中 bss段全部置零。

            堆(heap:堆是用于存放進程運行中被動態(tài)分配的內(nèi)存段,它的大小并不固定,可動態(tài)擴張或縮減。當進程調(diào)用malloc等函數(shù)分配內(nèi)存時,新分配的內(nèi)存就被動態(tài)添加到堆上(堆被擴張);當利用free等函數(shù)釋放內(nèi)存時,被釋放的內(nèi)存從堆中被剔除(堆被縮減)

            是用戶存放程序臨時創(chuàng)建的局部變量,也就是說我們函數(shù)括弧“{}”中定義的變量(但不包括static聲明的變量,static意味著在數(shù)據(jù)段中存放變量)。除此以外,在函數(shù)被調(diào)用時,其參數(shù)也會被壓入發(fā)起調(diào)用的進程中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值也會被存放回中。由于的先進先出特點,所以特別方便用來保存/恢復調(diào)用現(xiàn)場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數(shù)據(jù)的內(nèi)存區(qū)。

            進程如何組織這些區(qū)域?

            上述幾種內(nèi)存區(qū)域中數(shù)據(jù)段、BSS和堆通常是被連續(xù)存儲的——內(nèi)存位置上是連續(xù)的,而代碼段和往往會被獨立存放。有趣的是,堆和兩個區(qū)域關系很“曖昧”,他們一個向下“長”(i386體系結(jié)構(gòu)中向下、堆向上),一個向上“長”,相對而生。但你不必擔心他們會碰頭,因為他們之間間隔很大(到底大到多少,你可以從下面的例子程序計算一下),絕少有機會能碰到一起。

            下圖簡要描述了進程內(nèi)存區(qū)域的分布:

            數(shù)據(jù)段

            BSS

            代碼段

            “事實勝于雄辯”,我們用一個小例子(原形取自《User-Level Memory Management)來展示上面所講的各種內(nèi)存區(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;

             }

            它的結(jié)果如下

            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命令也可以看到程序的各段大小,比如執(zhí)行size example會得到

            text data bss dec hex filename

            1654 280   8 1942 796 example

            但這些數(shù)據(jù)是程序編譯的靜態(tài)統(tǒng)計,而上面顯示的是進程運行時的動態(tài)值,但兩者是對應的。

            通過前面的例子,我們對進程使用的邏輯內(nèi)存分布已先睹為快。這部分我們就繼續(xù)進入操作系統(tǒng)內(nèi)核看看,進程對內(nèi)存具體是如何進行分配和管理的。

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

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

            2.     進程地址如何映射到物理內(nèi)存?

            3.     物理內(nèi)存如何被管理?

            以及由上述問題引發(fā)的一些子問題。如系統(tǒng)虛擬地址分布內(nèi)存分配接口;連續(xù)內(nèi)存分配與非連續(xù)內(nèi)存分配等。

             

            進程內(nèi)存空間

            Linux操作系統(tǒng)采用虛擬內(nèi)存管理技術(shù),使得每個進程都有各自互不干涉的進程地址空間。該空間是塊大小為4G的線性虛擬空間,用戶所看到和接觸到的都是該虛擬地址,無法看到實際的物理內(nèi)存地址。利用這種虛擬地址不但能起到保護操作系統(tǒng)的效果(用戶不能直接訪問物理內(nèi)存),而且更重要的是,用戶程序可使用比實際物理內(nèi)存更大的地址空間(具體的原因請看硬件基礎部分)。

            在討論進程空間細節(jié)前,這里先要澄清下面幾個問題:

            l         第一、4G的進程地址空間被人為的分為兩個部分——用戶空間與內(nèi)核空間。用戶空間從03G0xC0000000),內(nèi)核空間占據(jù)3G4G。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內(nèi)核空間虛擬地址。只有用戶進程進行系統(tǒng)調(diào)用(代表用戶進程在內(nèi)核態(tài)執(zhí)行)等時刻可以訪問到內(nèi)核空間。

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

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

             

            進程內(nèi)存管理

            進程內(nèi)存管理的對象是進程線性地址空間上的內(nèi)存鏡像這些內(nèi)存鏡像其實就是進程使用的虛擬內(nèi)存區(qū)域(memory region)。進程虛擬空間是個3264位的“平坦”(獨立的連續(xù)區(qū)間)地址空間(空間的具體大小取決于體系結(jié)構(gòu))。要統(tǒng)一管理這么大的平坦空間可絕非易事,為了方便管理,虛擬空間被劃分為許多大小可變的(但必須是4096的倍數(shù))內(nèi)存區(qū)域,這些區(qū)域在進程線性地址中像停車位一樣有序排列。這些區(qū)域的劃分原則是“將訪問屬性一致的地址空間存放在一起”,所謂訪問屬性在這里無非指的是“可讀、可寫、可執(zhí)行等”。

            如果你要查看某個進程占用的內(nèi)存區(qū)域,可以使用命令cat /proc/<pid>/maps獲得(pid是進程號,你可以運行上面我們給出的例子——./example &;pid便會打印到屏幕),你可以發(fā)現(xiàn)很多類似于下面的數(shù)字信息。

            由于程序example使用了動態(tài)庫,所以除了example本身使用的內(nèi)存區(qū)域外,還會包含那些動態(tài)庫使用的內(nèi)存區(qū)域(區(qū)域順序是:代碼段、數(shù)據(jù)段、bss段)。

            我們下面只抽出和example有關的信息,除了前兩行代表的代碼段和數(shù)據(jù)段外,最后一行是進程使用的空間。

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

            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

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

            每行數(shù)據(jù)格式如下:

            (內(nèi)存區(qū)域)開始-結(jié)束 訪問權(quán)限  偏移  主設備號:次設備號 i節(jié)點  文件。

            注意,你一定會發(fā)現(xiàn)進程空間只包含三個內(nèi)存區(qū)域,似乎沒有上面所提到的堆、bss等,其實并非如此,程序內(nèi)存段和進程地址空間中的內(nèi)存區(qū)域是種模糊對應,也就是說,堆、bss、數(shù)據(jù)段(初始化過的)都在進程空間中由數(shù)據(jù)段內(nèi)存區(qū)域表示。

            Linux內(nèi)核中對應進程內(nèi)存區(qū)域的數(shù)據(jù)結(jié)構(gòu)是: vm_area_struct, 內(nèi)核將每個內(nèi)存區(qū)域作為一個單獨的內(nèi)存對象管理,相應的操作也都一致。采用面向?qū)ο蠓椒ㄊ?/span>VMA結(jié)構(gòu)體可以代表多種類型的內(nèi)存區(qū)域--比如內(nèi)存映射文件或進程的用戶空間棧等,對這些區(qū)域的操作也都不盡相同。

            vm_area_strcut結(jié)構(gòu)比較復雜,關于它的詳細結(jié)構(gòu)請參閱相關資料。我們這里只對它的組織方法做一點補充說明。vm_area_struct是描述進程地址空間的基本管理單元,對于一個進程來說往往需要多個內(nèi)存區(qū)域來描述它的虛擬空間,如何關聯(lián)這些不同的內(nèi)存區(qū)域呢?大家可能都會想到使用鏈表,的確vm_area_struct結(jié) 構(gòu)確實是以鏈表形式鏈接,不過為了方便查找,內(nèi)核又以紅黑樹(以前的內(nèi)核使用平衡樹)的形式組織內(nèi)存區(qū)域,以便降低搜索耗時。并存的兩種組織形式,并非冗 余:鏈表用于需要遍歷全部節(jié)點的時候用,而紅黑樹適用于在地址空間中定位特定內(nèi)存區(qū)域的時候。內(nèi)核為了內(nèi)存區(qū)域上的各種不同操作都能獲得高性能,所以同時 使用了這兩種數(shù)據(jù)結(jié)構(gòu)。

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

            mmap

            進程內(nèi)存描述符

             

            Vm_area_struct

            進程虛擬地址

            進程的地址空間對應的描述結(jié)構(gòu)是“內(nèi)存描述符結(jié)構(gòu),它表示進程的全部地址空間,——包含了和進程地址空間有關的全部信息,其中當然包含進程的內(nèi)存區(qū)域。

            進程內(nèi)存的分配與回收

            創(chuàng)建進程fork()、程序載入execve()、映射文件mmap()、動態(tài)內(nèi)存分配malloc()/brk()等進程相關操作都需要分配內(nèi)存給進程。不過這時進程申請和獲得的還不是實際內(nèi)存,而是虛擬內(nèi)存,準確的說是“內(nèi)存區(qū)域”。進程對內(nèi)存區(qū)域的分配最終都會歸結(jié)到do_mmap()函數(shù)上來(brk調(diào)用被單獨以系統(tǒng)調(diào)用實現(xiàn),不用do_mmap()),

            內(nèi)核使用do_mmap()函數(shù)創(chuàng)建一個新的線性地址區(qū)間。但是說該函數(shù)創(chuàng)建了一個新VMA并不非常準確,因為如果創(chuàng)建的地址區(qū)間和一個已經(jīng)存在的地址區(qū)間相鄰,并且它們具有相同的訪問權(quán)限的話,那么兩個區(qū)間將合并為一個。如果不能合并,那么就確實需要創(chuàng)建一個新的VMA了。但無論哪種情況, do_mmap()函數(shù)都會將一個地址區(qū)間加入到進程的地址空間中--無論是擴展已存在的內(nèi)存區(qū)域還是創(chuàng)建一個新的區(qū)域。

            同樣,釋放一個內(nèi)存區(qū)域應使用函數(shù)do_ummap()它會銷毀對應的內(nèi)存區(qū)域。

            如何由虛變實!

                從上面已經(jīng)看到進程所能直接操作的地址都為虛擬地址。當進程需要內(nèi)存時,從內(nèi)核獲得的僅僅是虛擬的內(nèi)存區(qū)域,而不是實際的物理地址,進程并沒有獲得物理內(nèi)存(物理頁面——頁的概念請大家參考硬件基礎一章),獲得的僅僅是對一個新的線性地址區(qū)間的使用權(quán)。實際的物理內(nèi)存只有當進程真的去訪問新獲取的虛擬地址時,才會由“請求頁機制”產(chǎn)生“缺頁”異常,從而進入分配實際頁面的例程。

            該異常是虛擬內(nèi)存機制賴以存在的基本保證——它會告訴內(nèi)核去真正為進程分配物理頁,并建立對應的頁表,這之后虛擬地址才實實在在地映射到了系統(tǒng)的物理內(nèi)存上。(當然,如果頁被換出到磁盤,也會產(chǎn)生缺頁異常,不過這時不用再建立頁表了)

            這種請求頁機制把頁面的分配推遲到不能再推遲為止,并不急于把所有的事情都一次做完(這種思想有點像設計模式中的代理模式(proxy))。之所以能這么做是利用了內(nèi)存訪問的“局部性原理”,請求頁帶來的好處是節(jié)約了空閑內(nèi)存,提高了系統(tǒng)的吞吐率。要想更清楚地了解請求頁機制,可以看看《深入理解linux內(nèi)核》一書。

            這里我們需要說明在內(nèi)存區(qū)域結(jié)構(gòu)上的nopage操作。當訪問的進程虛擬內(nèi)存并未真正分配頁面時,該操作便被調(diào)用來分配實際的物理頁,并為該頁建立頁表項。在最后的例子中我們會演示如何使用該方法。

             

             

            系統(tǒng)物理內(nèi)存管理 

            雖 然應用程序操作的對象是映射到物理內(nèi)存之上的虛擬內(nèi)存,但是處理器直接操作的卻是物理內(nèi)存。所以當應用程序訪問一個虛擬地址時,首先必須將虛擬地址轉(zhuǎn)化成 物理地址,然后處理器才能解析地址訪問請求。地址的轉(zhuǎn)換工作需要通過查詢頁表才能完成,概括地講,地址轉(zhuǎn)換需要將虛擬地址分段,使每段虛地址都作為一個索 引指向頁表,而頁表項則指向下一級別的頁表或者指向最終的物理頁面。

            每個進程都有自己的頁表。進程描述符的pgd域指向的就是進程的頁全局目錄。下面我們借用《linux設備驅(qū)動程序》中的一幅圖大致看看進程地址空間到物理頁之間的轉(zhuǎn)換關系。

             

             

                 上面的過程說起來簡單,做起來難呀。因為在虛擬地址映射到頁之前必須先分配物理頁——也就是說必須先從內(nèi)核中獲取空閑頁,并建立頁表。下面我們介紹一下內(nèi)核管理物理內(nèi)存的機制。

             

            物理內(nèi)存管理(頁管理)

            Linux內(nèi)核管理物理內(nèi)存是通過分頁機制實現(xiàn)的,它將整個內(nèi)存劃分成無數(shù)個4k(i386體系結(jié)構(gòu)中)大小的頁,從而分配和回收內(nèi)存的基本單位便是內(nèi)存頁了。利用分頁管理有助于靈活分配內(nèi)存地址,因為分配時不必要求必須有大塊的連續(xù)內(nèi)存[3],系統(tǒng)可以東一頁、西一頁的湊出所需要的內(nèi)存供進程使用。雖然如此,但是實際上系統(tǒng)使用內(nèi)存時還是傾向于分配連續(xù)的內(nèi)存塊,因為分配連續(xù)內(nèi)存時,頁表不需要更改,因此能降低TLB的刷新率(頻繁刷新會在很大程度上降低訪問速度)。

            鑒于上述需求,內(nèi)核分配物理頁面時為了盡量減少不連續(xù)情況,采用了“伙伴”關系來管理空閑頁面。伙伴關系分配算法大家應該不陌生——幾乎所有操作系統(tǒng)方面的書都會提到,我們不去詳細說它了,如果不明白可以參看有關資料。這里只需要大家明白Linux中空閑頁面的組織和管理利用了伙伴關系,因此空閑頁面分配時也需要遵循伙伴關系,最小單位只能是2的冪倍頁面大小。內(nèi)核中分配空閑頁面的基本函數(shù)是get_free_page/get_free_pages,它們或是分配單頁或是分配指定的頁面(248…512頁)。

             注意:get_free_page是在內(nèi)核中分配內(nèi)存,不同于malloc在用戶空間中分配,malloc利用堆動態(tài)分配,實際上是調(diào)用brk()系統(tǒng)調(diào)用,該調(diào)用的作用是擴大或縮小進程堆空間(它會修改進程的brk域)。如果現(xiàn)有的內(nèi)存區(qū)域不夠容納堆空間,則會以頁面大小的倍數(shù)為單位,擴張或收縮對應的內(nèi)存區(qū)域,但brk值并非以頁面大小為倍數(shù)修改,而是按實際請求修改。因此Malloc在用戶空間分配內(nèi)存可以以字節(jié)為單位分配,但內(nèi)核在內(nèi)部仍然會是以頁為單位分配的。

               另外,需要提及的是,物理頁在系統(tǒng)中由頁結(jié)構(gòu)struct page描述,系統(tǒng)中所有的頁面都存儲在數(shù)組mem_map[]中,可以通過該數(shù)組找到系統(tǒng)中的每一頁(空閑或非空閑)。而其中的空閑頁面則可由上述提到的以伙伴關系組織的空閑頁鏈表(free_area[MAX_ORDER]索引。

             

            空閑頁框

            APP

            內(nèi)存區(qū)域 vm_area_structs

            mallocforkexcutemmap

            brk/do_map

            get_free_page(s)

            用戶空間

            內(nèi)核空間

            進程虛擬地址空間

             

            系統(tǒng)調(diào)用

            進程頁表

             

            請頁異常

            文本框: 伙伴關系維護

            內(nèi)核內(nèi)存使用

            Slab

                所 謂尺有所長,寸有所短。以頁為最小單位分配內(nèi)存對于內(nèi)核管理系統(tǒng)中的物理內(nèi)存來說的確比較方便,但內(nèi)核自身最常使用的內(nèi)存卻往往是很小(遠遠小于一頁)的 內(nèi)存塊——比如存放文件描述符、進程描述符、虛擬內(nèi)存區(qū)域描述符等行為所需的內(nèi)存都不足一頁。這些用來存放描述符的內(nèi)存相比頁面而言,就好比是面包屑與面 包。一個整頁中可以聚集多個這些小塊內(nèi)存;而且這些小塊內(nèi)存塊也和面包屑一樣頻繁地生成/銷毀。

              為了滿足內(nèi)核對這種小內(nèi)存塊的需要,Linux系統(tǒng)采用了一種被稱為slab分配器的技術(shù)。Slab分配器的實現(xiàn)相當復雜,但原理不難,其核心思想就是“存儲池[4]的運用。內(nèi)存片段(小塊內(nèi)存)被看作對象,當被使用完后,并不直接釋放而是被緩存到“存儲池”里,留做下次使用,這無疑避免了頻繁創(chuàng)建與銷毀對象所帶來的額外負載。

            Slab技術(shù)不但避免了內(nèi)存內(nèi)部分片(下文將解釋)帶來的不便(引入Slab分配器的主要目的是為了減少對伙伴系統(tǒng)分配算法的調(diào)用次數(shù)——頻繁分配和回收必然會導致內(nèi)存碎片——難以找到大塊連續(xù)的可用內(nèi)存,而且可以很好地利用硬件緩存提高訪問速度。

                Slab并非是脫離伙伴關系而獨立存在的一種內(nèi)存分配方式,slab仍然是建立在頁面基礎之上,換句話說,Slab將頁面(來自于伙伴關系管理的空閑頁面鏈表)撕碎成眾多小內(nèi)存塊以供分配,slab中的對象分配和銷毀使用kmem_cache_allockmem_cache_free

            Kmalloc

            Slab分配器不僅僅只用來存放內(nèi)核專用的結(jié)構(gòu)體,它還被用來處理內(nèi)核對小塊內(nèi)存的請求。當然鑒于Slab分配器的特點,一般來說內(nèi)核程序中對小于一頁的小塊內(nèi)存的請求才通過Slab分配器提供的接口Kmalloc來完成(雖然它可分配32 131072字節(jié)的內(nèi)存)。從內(nèi)核內(nèi)存分配的角度來講,kmalloc可被看成是get_free_pages)的一個有效補充,內(nèi)存分配粒度更靈活了。

            有興趣的話,可以到/proc/slabinfo中找到內(nèi)核執(zhí)行現(xiàn)場使用的各種slab信息統(tǒng)計,其中你會看到系統(tǒng)中所有slab的使用信息。從信息中可以看到系統(tǒng)中除了專用結(jié)構(gòu)體使用的slab外,還存在大量為Kmalloc而準備的Slab(其中有些為dma準備的)。

             

             

            內(nèi)核非連續(xù)內(nèi)存分配(Vmalloc

             

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

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

            與用戶進程相似,內(nèi)核也有一個名為init_mmmm_strcut結(jié)構(gòu)來描述內(nèi)核地址空間,其中頁表項pdg=swapper_pg_dir包含了系統(tǒng)內(nèi)核空間(3G-4G)的映射關系。因此vmalloc分配內(nèi)核虛擬地址必須更新內(nèi)核頁表,而kmallocget_free_page由于分配的連續(xù)內(nèi)存,所以不需要更新內(nèi)核頁表。

            空閑頁框

            APP

            內(nèi)存區(qū)域 vm_area_structs

            mallocforkexcutemmap

            brk/do_map

            get_free_page(s)

            用戶空間

            內(nèi)核空間

            進程虛擬地址空間

             

            系統(tǒng)調(diào)用

            進程頁表

             

            請頁異常

            內(nèi)核程序

            物理內(nèi)存影射區(qū)

            Vmalloc分配區(qū)

            slab

            get_free_page(s)

            內(nèi)核頁表

            get_free_page(s)

            請頁異常

            文本框: 伙伴關系維護文本框: vmalloc文本框: kmalloc

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

             

             

            進程地址空間

            物理內(nèi)存映射區(qū)

             

            3G

            內(nèi)核虛擬空間

            Vmalloc_start

            Vmalloc_end

            上圖是內(nèi)存分布的模糊輪廓

             

               get_free_pageKmalloc函數(shù)所分配的連續(xù)內(nèi)存都陷于物理映射區(qū)域,所以它們返回的內(nèi)核虛擬地址和實際物理地址僅僅是相差一個偏移量(PAGE_OFFSET),你可以很方便的將其轉(zhuǎn)化為物理內(nèi)存地址,同時內(nèi)核也提供了virt_to_phys()函數(shù)將內(nèi)核虛擬空間中的物理映射區(qū)地址轉(zhuǎn)化為物理地址。要知道,物理內(nèi)存映射區(qū)中的地址與內(nèi)核頁表是有序?qū)模到y(tǒng)中的每個物理頁面都可以找到它對應的內(nèi)核虛擬地址(在物理內(nèi)存映射區(qū)中的)。

            vmalloc分配的地址則限于vmalloc_startvmalloc_end之間。每一塊vmalloc分配的內(nèi)核虛擬內(nèi)存都對應一個vm_struct結(jié)構(gòu)體(可別和vm_area_struct搞混,那可是進程虛擬內(nèi)存區(qū)域的結(jié)構(gòu)),不同的內(nèi)核虛擬地址被4k大小的空閑區(qū)間隔,以防止越界——見下圖)。與進程虛擬地址的特性一樣,這些虛擬地址與物理內(nèi)存沒有簡單的位移關系,必須通過內(nèi)核頁表才可轉(zhuǎn)換為物理地址或物理頁。它們有可能尚未被映射,在發(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);

            }

             

            實例

            內(nèi)存映射(mmap)Linux操作系統(tǒng)的一個很大特色,它可以將系統(tǒng)內(nèi)存映射到一個文件(設備)上,以便可以通過訪問文件內(nèi)容來達到訪問內(nèi)存的目的。這樣做的最大好處是提高了內(nèi)存訪問速度,并且可以利用文件系統(tǒng)的接口編程(設備在Linux中 作為特殊文件處理)訪問內(nèi)存,降低了開發(fā)難度。許多設備驅(qū)動程序便是利用內(nèi)存映射功能將用戶空間的一段地址關聯(lián)到設備內(nèi)存上,無論何時,只要內(nèi)存在分配的 地址范圍內(nèi)進行讀寫,實際上就是對設備內(nèi)存的訪問。同時對設備文件的訪問也等同于對內(nèi)存區(qū)域的訪問,也就是說,通過文件操作接口可以訪問內(nèi)存。Linux中的X服務器就是一個利用內(nèi)存映射達到直接高速訪問視頻卡內(nèi)存的例子。

            熟悉文件操作的朋友一定會知道file_operations結(jié)構(gòu)中有mmap方法,在用戶執(zhí)行mmap系統(tǒng)調(diào)用時,便會調(diào)用該方法來通過文件訪問內(nèi)存——不過在調(diào)用文件系統(tǒng)mmap方法前,內(nèi)核還需要處理分配內(nèi)存區(qū)域(vma_struct)、建立頁表等工作。對于具體映射細節(jié)不作介紹了,需要強調(diào)的是,建立頁表可以采用remap_page_range方法一次建立起所有映射區(qū)的頁表,或利用vma_structnopage方法在缺頁時現(xiàn)場一頁一頁的建立頁表。第一種方法相比第二種方法簡單方便、速度快, 但是靈活性不高。一次調(diào)用所有頁表便定型了,不適用于那些需要現(xiàn)場建立頁表的場合——比如映射區(qū)需要擴展或下面我們例子中的情況。

             

            我們這里的實例希望利用內(nèi)存映射,將系統(tǒng)內(nèi)核中的一部分虛擬內(nèi)存映射到用戶空間,以供應用程序讀取——你可利用它進行內(nèi)核空間到用戶空間的大規(guī)模信息傳輸。因此我們將試圖寫一個虛擬字符設備驅(qū)動程序,通過它將系統(tǒng)內(nèi)核空間映射到用戶空間——將內(nèi)核虛擬內(nèi)存映射到用戶虛擬地址。從上一節(jié)已經(jīng)看到Linux內(nèi)核空間中包含兩種虛擬地址:一種是物理和邏輯都連續(xù)的物理內(nèi)存映射虛擬地址;另一種是邏輯連續(xù)但非物理連續(xù)的vmalloc分配的內(nèi)存虛擬地址。我們的例子程序?qū)⒀菔景?/span>vmalloc分配的內(nèi)核虛擬地址映射到用戶地址空間的全過程。

            程序里主要應解決兩個問題:

            第一是如何將vmalloc分配的內(nèi)核虛擬內(nèi)存正確地轉(zhuǎn)化成物理地址?

            因為內(nèi)存映射先要獲得被映射的物理地址,然后才能將其映射到要求的用戶虛擬地址上。我們已經(jīng)看到內(nèi)核物理內(nèi)存映射區(qū)域中的地址可以被內(nèi)核函數(shù)virt_to_phys轉(zhuǎn)換成實際的物理內(nèi)存地址,但對于vmalloc分配的內(nèi)核虛擬地址無法直接轉(zhuǎn)化成物理地址,所以我們必須對這部分虛擬內(nèi)存格外“照顧”——先將其轉(zhuǎn)化成內(nèi)核物理內(nèi)存映射區(qū)域中的地址,然后在用virt_to_phys變?yōu)槲锢淼刂贰?/span>

            轉(zhuǎn)化工作需要進行如下步驟:

            a)         找到vmalloc虛擬內(nèi)存對應的頁表,并尋找到對應的頁表項。

            b)        獲取頁表項對應的頁面指針

            c)        通過頁面得到對應的內(nèi)核物理內(nèi)存映射區(qū)域地址

            如下圖所示:

            Virtu_to_phys地址影射

                          用戶進程地址空間

            內(nèi)核物理地址影射區(qū)

            內(nèi)核虛擬地址影射區(qū)

            地址轉(zhuǎn)換

             

            第二是當訪問vmalloc分配區(qū)時,如果發(fā)現(xiàn)虛擬內(nèi)存尚未被映射到物理頁,則需要處理“缺頁異常”。因此需要我們實現(xiàn)內(nèi)存區(qū)域中的nopaga操作,以能返回被映射的物理頁面指針,在我們的實例中就是返回上面過程中的內(nèi)核物理內(nèi)存映射區(qū)域中的地址由于vmalloc分配的虛擬地址與物理地址的對應關系并非分配時就可確定,必須在缺頁現(xiàn)場建立頁表,因此這里不能使用remap_page_range方法,只能用vmanopage方法一頁一頁的建立。

             

             

            程序組成

            map_driver.c,它是以模塊形式加載的虛擬字符驅(qū)動程序。該驅(qū)動負責將一定長的內(nèi)核虛擬地址(vmalloc分配的)映射到設備文件上。其中主要的函數(shù)有——vaddress_to_kaddress()負責對vmalloc分配的地址進行頁表解析,以找到對應的內(nèi)核物理映射地址(kmalloc分配的地址);map_nopage()負責在進程訪問一個當前并不存在的VMA頁時,尋找該地址對應的物理頁,并返回該頁的指針。

            test.c 它利用上述驅(qū)動模塊對應的設備文件在用戶空間讀取讀取內(nèi)核內(nèi)存。結(jié)果可以看到內(nèi)核虛擬地址的內(nèi)容(ok!),被顯示在了屏幕上。

             

            執(zhí)行步驟

            編譯map_driver.cmap_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文件,將取自內(nèi)核的信息打印到屏幕上。

             

            全部程序下載 mmap.tar (感謝Martin Frey,該程序的主體出自他的靈感)

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             

             



            [1] 靜態(tài)分配內(nèi)存就是編譯器在編譯程序的時候根據(jù)源程序來分配內(nèi)存. 動態(tài)分配內(nèi)存就是在程序編譯之后, 運行時調(diào)用運行時刻庫函數(shù)來分配內(nèi)存的. 靜態(tài)分配由于是在程序運行之前,所以速度快, 效率高, 但是局限性大. 動態(tài)分配在程序運行時執(zhí)行, 所以速度慢, 但靈活性高.

             

            [2]術(shù)語"BSS"已經(jīng)有些年頭了,它是block started by symbol的縮寫。因為未初始化的變量沒有對應的值,所以并不需要存儲在可執(zhí)行對象中。但是因為C標準強制規(guī)定未初始化的全局變量要被賦予特殊的默認值(基本上是0),所以內(nèi)核要從可執(zhí)行代碼裝入變量(未賦值的)到內(nèi)存中,然后將零頁映射到該片內(nèi)存上,于是這些未初始化變量就被賦予了0值。這樣做避免了在目標文件中進行顯式地初始化,減少空間浪費(來自《Linux內(nèi)核開發(fā)》)

            [3] 還有些情況必須要求內(nèi)存連續(xù),比如DMA傳輸中使用的內(nèi)存,由于不涉及頁機制所以必須連續(xù)分配。

            [4] 這種存儲池的思想在計算機科學里廣泛應用,比如數(shù)據(jù)庫連接池、內(nèi)存訪問池等等。

            posted on 2008-02-26 15:32 Magic 閱讀(505) 評論(0)  編輯 收藏 引用 所屬分類: Linux
            国产一级做a爰片久久毛片| 久久久久99这里有精品10| 久久精品国产亚洲AV影院| 区久久AAA片69亚洲| 性欧美大战久久久久久久久| 久久99久久99小草精品免视看| 国产一级持黄大片99久久| 久久久久久久91精品免费观看| 久久久久久毛片免费播放| 精品久久久久久无码国产| 99精品国产99久久久久久97| 青青青青久久精品国产| 久久亚洲精品无码aⅴ大香| 办公室久久精品| 久久久久久久亚洲Av无码| 久久久久亚洲精品日久生情 | 久久午夜无码鲁丝片午夜精品| 久久久噜噜噜久久中文字幕色伊伊 | 久久99国产综合精品女同| 久久99亚洲综合精品首页| 久久精品午夜一区二区福利| 亚洲美日韩Av中文字幕无码久久久妻妇| 韩国免费A级毛片久久| 亚洲国产精品成人久久蜜臀| 亚洲国产精品久久久久婷婷软件| 久久亚洲精品无码AV红樱桃| 欧美日韩久久中文字幕| 久久99国产精品久久99小说| 久久中文精品无码中文字幕| 亚洲欧美日韩精品久久| 久久福利青草精品资源站| 久久婷婷久久一区二区三区| 国产成人精品免费久久久久| 精品国产乱码久久久久久1区2区| 狠狠色狠狠色综合久久| 久久精品国产亚洲AV影院| 久久人人爽人人爽人人片AV不| 久久免费视频1| 色婷婷久久综合中文久久蜜桃av| 精品久久久无码人妻中文字幕| 伊人久久精品无码二区麻豆|