Linux中的內存管理模型

轉自:http://weli.iteye.com/blog/1692038

在weibo上看到樑大的這個貼子: 

 

實際上這是一個內存方面的問題。要想研究這個問題,首先我們要將題目本身搞明白。由於我對Linux內核比較熟而對Windows的內存模型幾乎毫不瞭解,因此在這篇文章中針對Linux環境對這個問題進行探討。

在Linux的世界中,從大的方面來講,有兩塊內存,一塊叫做內核空間,Kernel Space,另一塊叫做用戶空間,即User Space。它們是相互獨立的,Kernel對它們的管理方式也完全不同。 


首先我們要知道,現代操作系統一個重要的任務之一就是管理內存。所謂內存,就是內存條上一個一個的真正的存儲單元,實實在在的電子顆粒,這裏面通過電信號保存着數據。 

Linux Kernel爲了使用和管理這些內存,必須要給它們分成一個一個的小塊,然後給這些小塊標號。這一個一個的小塊就叫做Page,標號就是內存地址,Address。 

Linux內核會負責管理這些內存,保證程序可以有效地使用這些內存。它必須要能夠管理好內核本身要用的內存,同時也要管理好在Linux操作系統上面跑的各種程序使用的內存。因此,Linux將內存劃分爲Kernel Space和User Space,對它們分別進行管理。 

只有驅動模塊和內核本身運行在Kernel Space當中,因此對於這道題目,我們主要進行考慮的是User Space這一塊。 

在Linux的世界中,Kernel負責給用戶層的程序提供虛地址而不是物理地址。舉個例子:A手裏有20張牌,將它們命名爲1-20。這20張牌要分給兩個人,每個人手裏10張。這樣,第一個人拿到10張牌,將牌編號爲1-10,對應A手裏面的1-10;第二個人拿到10張牌,也給編號爲1-10,對應A的11-20。 

這裏面,第二個人手裏的牌,他自己用的時候編號是1-10,但A知道,第二個人手裏的牌在他這裏的編號是11-20。 

在這裏面,A的角色就是Linux內核;他手裏的編號,1-20,就是物理地址;兩個人相當於兩個進程,它們對牌的編號就是虛地址;A要負責給兩個人發牌,這就是內存管理。 

瞭解了這些概念以後,我們來看看kernel當中具體的東西,首先是mm_struct這個結構體: 

C代碼  收藏代碼
  1. struct mm_struct {  
  2.         struct vm_area_struct * mmap;           /* list of VMAs */  
  3.         struct rb_root mm_rb;  
  4.         struct vm_area_struct * mmap_cache;     /* last find_vma result */  
  5.         ...  
  6.         unsigned long start_code, end_code, start_data, end_data;  
  7.         unsigned long start_brk, brk, start_stack;  
  8.         ...  
  9. };  


mm_struct負責描述進程的內存。相當於發牌人記錄給誰發了哪些牌,發了多少張,等等。那麼,內存是如何將內存進行劃分的呢?也就是說,發牌人手裏假設是一大張未裁剪的撲克紙,他是怎樣將其剪成一張一張的撲克牌呢?上面的vm_area_struct就是基本的劃分單位,即一張一張的撲克牌: 

C代碼  收藏代碼
  1. struct vm_area_struct * mmap;  


這個結構體的定義如下: 

C代碼  收藏代碼
  1. struct vm_area_struct {  
  2.         struct mm_struct * vm_mm;       /* The address space we belong to. */  
  3.         unsigned long vm_start;         /* Our start address within vm_mm. */  
  4.         unsigned long vm_end;           /* The first byte after our end address 
  5.                                            within vm_mm. */  
  6.         ....  
  7.         /* linked list of VM areas per task, sorted by address */  
  8.         struct vm_area_struct *vm_next;  
  9.         ....  
  10. }  


這樣,內核就可以記錄分配給用戶空間的內存了。 

Okay,瞭解了內核管理進程內存的兩個最重要的結構體,我們來看看用戶空間的內存模型。 

Linux操作系統在加載程序時,將程序所使用的內存分爲5段:text(程序段)、data(數據段)、bss(bss數據段)、heap(堆)、stack(棧)。 


text segment(程序段) 

text segment用於存放程序指令本身,Linux在執行程序時,要把這個程序的代碼加載進內存,放入text segment。程序段內存位於整個程序所佔內存的最上方,並且長度固定(因爲代碼需要多少內存給放進去,操作系統是清楚的)。 

data segment(數據段) 

data segment用於存放已經在代碼中賦值的全局變量和靜態變量。因爲這類變量的數據類型(需要的內存大小)和其數值都已在代碼中確定,因此,data segment緊挨着text segment,並且長度固定(這塊需要多少內存也已經事先知道了)。 


bss segment(bss數據段) 

bss segment用於存放未賦值的全局變量和靜態變量。這塊挨着data segment,長度固定。 

heap(堆) 

這塊內存用於存放程序所需的動態內存空間,比如使用malloc函數請求內存空間,就是從heap裏面取。這塊內存挨着bss,長度不確定。 

stack(棧) 

stack用於存放局部變量,當程序調用某個函數(包括main函數)時,這個函數內部的一些變量的數值入棧,函數調用完成返回後,局部變量的數值就沒有用了,因此出棧,把內存讓出來給另一個函數的變量使用(程序在執行時,總是會在某一個函數調用裏面)。 

我們看一個圖例說明: 

 

爲了更好的理解內存分段,可以撰寫一段代碼: 

C代碼  收藏代碼
  1. #include <stdio.h>  
  2.   
  3. // 未賦值的全局變量放在dss段  
  4. int global_var;  
  5.   
  6. // 已賦值的全局變量放在data段  
  7. int global_initialized_var = 5;  
  8.   
  9. void function() {    
  10.    int stack_var; // 函數中的變量放在stack中  
  11.   
  12.    // 放在stack中的變量  
  13.    // 顯示其所在內存地值  
  14.    printf("the function's stack_var is at address 0x%08x\n", &stack_var);  
  15. }  
  16.   
  17.   
  18. int main() {  
  19.    int stack_var; // 函數中的變量放在stack中  
  20.      
  21.    // 已賦值的靜態變量放在data段  
  22.    static int static_initialized_var = 5;  
  23.      
  24.    // 未賦值的靜態變量放在dss段  
  25.    static int static_var;  
  26.      
  27.    int *heap_var_ptr;  
  28.   
  29.    // 由malloc在heap中分配所需內存,  
  30.    // heap_var_ptr這個指針指向這塊  
  31.    // 分配的內存  
  32.    heap_var_ptr = (int *) malloc(4);  
  33.   
  34.    // 放在data段的變量  
  35.    // 顯示其所在內存地值  
  36.    printf("====IN DATA SEGMENT====\n");  
  37.    printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);  
  38.    printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);  
  39.   
  40.    // 放在bss段的變量  
  41.    // 顯示其所在內存地值  
  42.    printf("====IN BSS SEGMENT====\n");  
  43.    printf("static_var is at address 0x%08x\n", &static_var);  
  44.    printf("global_var is at address 0x%08x\n\n", &global_var);  
  45.   
  46.    // 放在heap中的變量  
  47.    // 顯示其所在內存地值  
  48.    printf("====IN HEAP====\n");  
  49.    printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);  
  50.   
  51.    // 放在stack中的變量  
  52.    // 顯示其所在內存地值  
  53.    printf("====IN STACK====\n");  
  54.    printf("the main's stack_var is at address 0x%08x\n", &stack_var);  
  55.    function();   
  56. }  


編譯這個代碼,看看執行結果: 



理解了進程的內存空間使用,我們現在可以想想,這幾塊內存當中,最靈活的是哪一塊?沒錯,是Heap。其它幾塊都由C編譯器編譯代碼時預處理,相對固定,而heap內存可以由malloc和free進行動態的分配和銷燬。 

有關malloc和free的使用方法,在本文中我就不再多說,這些屬於基本知識。我們在這篇文章中要關心的是,malloc是如何工作的?實際上,它會去調用mmap(),而mmap()則會調用內核,獲取VMA,即前文中看到的vm_area。這一塊工作由c庫向kernel發起請求,而由kernel完成這個請求,在kernel當中,有vm_operations_struct進行實際的內存操作: 

C代碼  收藏代碼
  1. struct vm_operations_struct {  
  2.         void (*open)(struct vm_area_struct * area);  
  3.         void (*close)(struct vm_area_struct * area);  
  4.         ...  
  5. };  


可以看到,kernel可以對VMA進行open和close,即收發牌的工作。理解了malloc的工作原理,free也不難了,它向下調用munmap()。 

下面是mmap和munmap的函數定義: 

C代碼  收藏代碼
  1. void *  
  2. mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);  


這裏面,addr是希望能夠分配到的虛地址,比如:我希望得到一張牌,做爲我手裏編號爲2的那張。需要注意的是,mmap最後分配出來的內存地址不一定是你想要的,可能你請求一張編號爲2的撲克,但發牌人控制這個編號過程,他會給你一張在你手裏編號爲3的撲克。 

prot代表對進程對這塊內存的權限: 

C代碼  收藏代碼
  1. PROT_READ 是否可讀  
  2. PROT_WRITE 是否可寫  
  3. PROT_EXEC IP指針是否可以指向這裏進行代碼的執行  
  4. PROT_NONE 不能訪問  


flags代表用於控制很多的內存屬性,我們一會兒會用到,這裏不展開。 

fd是文件描述符。我們這裏必須明白一個基本原理,任何硬盤上面的數據,都要讀取到內存當中,才能被程序使用,因此,mmap的目的就是將文件數據映射進內存。因此,要在這裏填寫文件描述符。如果你在這裏寫-1,則不映射任何文件數據,只是在內存裏面要上這一塊空間,這就是malloc對mmap的使用方法。 

offset是文件的偏移量,比如:從第二行開始映射。文件映射,不是這篇文章關心的內容,不展開。 

okay,瞭解了mmap的用法,下面看看munmap: 

C代碼  收藏代碼
  1. int  
  2. munmap(void *addr, size_t len);  


munmap很簡單,告訴它要還回去的內存地址(即哪張牌),然後告訴它還回去的數量(多少張),其實更準確的說:尺寸。 


現在讓我們回到題目上來,如何部分地回收一個數組中的內存?我們知道,使用malloc和free是無法完成的: 

C代碼  收藏代碼
  1. #include <stdlib.h>  
  2. int main() {  
  3.         int *p = malloc(12);  
  4.         free(p);  
  5.         return 0;  
  6. }  


因爲無論是malloc還是free,都需要我們整體提交待分配和銷燬的全部內存。於是自然而然想到,是否可以malloc分配內存後,然後使用munmap來部分地釋放呢?下面是一個嘗試: 

C代碼  收藏代碼
  1. #include <sys/mman.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4.   
  5. int main() {  
  6.     int *arr;  
  7.     int *p;  
  8.     p = arr = (int*) malloc(3 * sizeof(int));  
  9.     int i = 0;  
  10.       
  11.     for (i=0;i<3;i++) {  
  12.         *p = i;  
  13.         printf("address of arr[%d]: %p\n", i, p);  
  14.         p++;  
  15.     }  
  16.       
  17.     printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));  
  18. }  


運行這段代碼輸出如下: 

 

注意到munmap調用返回-1,說明內存釋放未成功,這是由於munmap處理的內存地址必須頁對齊(Page Aligned)。在Linux下面,kernel使用4096 byte來劃分頁面,而malloc的顆粒度更細,使用8 byte對齊,因此,分配出來的內存不一定是頁對齊的。爲了解決這個問題,我們可以使用memalign或是posix_memalign來獲取一塊頁對齊的內存: 

C代碼  收藏代碼
  1. #include <sys/mman.h>    
  2. #include <stdio.h>    
  3. #include <stdlib.h>    
  4.     
  5. int main() {    
  6.     void *arr;    
  7.     printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 4096));  
  8.     printf("address of arr: %p\n", arr);    
  9.     printf("munmap: %d\n", munmap(arr, 4096));    
  10. }   


運行上述代碼得結果如下: 

 

可以看到,頁對齊的內存資源可以被munmap正確處理(munmap返回值爲0,說明執行成功)。仔細看一下被分配出來的地址: 

Bash代碼  收藏代碼
  1. 0x7fe09b804000  


轉換到10進制是:140602658275328 

試試看是否能被4096整除:140602658275328 / 4096 = 34326820868 

可以被整除,驗證了分配出來的地址是頁對齊的。 

接下來,我們試用一下mmap,來分配一塊內存空間: 

C代碼  收藏代碼
  1. mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)  


注意上面mmap的使用方法。其中,我們不指定虛地址,讓內核決定內存地址,也就是說,我們要是要一張牌,但不關心給牌編什麼號。然後PROT_READ|PROT_WRITE表示這塊內存可讀寫,接下來注意flags裏面有MAP_ANONYMOUS,表示這塊內存不用於映射文件。下面是完整代碼: 

C代碼  收藏代碼
  1. #include <sys/mman.h>  
  2. #include <stdio.h>  
  3. #include <stdlib.h>  
  4.   
  5. int main() {  
  6.     int *arr;  
  7.     int *p;  
  8.     p = arr = (int*) mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);  
  9.     int i = 0;  
  10.       
  11.     for (i=0;i<3;i++) {  
  12.         *p = i;  
  13.         printf("address of arr[%d]: %p\n", i, p);  
  14.         p++;  
  15.     }  
  16.       
  17.     printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));  
  18. }  


運行結果如下: 

 

注意munmap返回值爲0,說明內存釋放成功了。因此,驗證了mmap分配出來的內存是頁對齊的。 

okay,瞭解了所有這些背景知識,我們現在應該對給內存打洞這個問題有一個思路了。我們可以創建以Page爲基本單元的內存空間,然後用munmap在上面打洞。下面是實驗代碼: 

C代碼  收藏代碼
  1. #include <sys/mman.h>    
  2. #include <stdio.h>    
  3. #include <stdlib.h>    
  4.     
  5. int main() {    
  6.     void *arr;    
  7.     printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 3 * 4096));  
  8.     printf("address of arr: %p\n", arr);   
  9.     printf("address of arr[4096]: %p\n", &arr[4096]);   
  10.     printf("munmap: %d\n", munmap(&arr[4096], 4096));    
  11. }    


我們申請了3*4096 byte的空間,也就是3頁的內存,然後通過munmap,在中間這頁上開個洞 。運行上面的代碼,結果如下: 

 

看到munmap的返回爲0,說明內存釋放成功,我們在arr數組上成功地開了一個洞。 

這種方法,最大的侷限在於,你操作的內存必須是page對齊的。如果想要更細顆粒度的打洞,純靠User Space的API調用是不行的,需要在Kernel Space直接操作進程的VMA結構體來實現。實現思路如下: 

1. 通過kernel提供的page map映射,找到要釋放的內存虛地址所對應的物理地址。 
2. 撰寫一個內核模塊,幫助你user space的程序來將實際的物理內存放回free list。 

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