Linux內存管理(1)

前言

內存管理一向是所有操作系統書籍不惜筆墨重點討論的內容,無論市面上或是網上都充斥着大量涉及內存管理的教材和資料。因此我們這裏所要寫的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的進程地址空間被人爲的分爲兩個部分——用戶空間與內核空間。用戶空間從03G0xC0000000),內核空間佔據3G4G。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內核空間虛擬地址。例外情況只有用戶進程進行系統調用(代表用戶進程在內核態執行)等時刻可以訪問到內核空間。

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

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

進程內存管理

進程內存管理的對象是進程線性地址空間上的內存鏡像,這些內存鏡像其實就是進程使用的虛擬內存區域(memory region)。進程虛擬空間是個3264位的“平坦”(獨立的連續區間)地址空間(空間的具體大小取決於體系結構)。要統一管理這麼大的平坦空間可絕非易事,爲了方便管理,虛擬空間被化分爲許多大小可變的(但必須是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結構確實是已鏈表形式鏈接,不過位了方便查找,內核又以紅黑樹(以前的內核使用平衡樹)的形式組織內存區域,以便降低搜索耗時。並存兩種組織形式,並非冗餘:鏈表用於需要遍歷全部節點的時候用,而紅黑樹適用於在地址空間中定位特定內存區域的時候。內核爲了內存區域上的各種不同操作都能獲得高性能,所以同時使用了這兩種數據結構。

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

mmap

進程內存描述符

Vm_area_struct

進程虛擬地址

進程的地址空間對應的描述結構是“內存描述符結構,它表示進程的全部地址空間,——包含了和進程地址空間有關的全部信息,其中當然包含進程的內存區域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章