內存管理概述、內存分配與釋放、地址映射機制(mm_struct, vm_area_struct)、malloc/free 的實現

http://www.cnblogs.com/zengkefu/p/5589799.html html

注:本分類下文章大多整理自《深刻分析linux內核源代碼》一書,另有參考其餘一些資料如《linux內核徹底剖析》、《linux c 編程一站式學習》等,只是爲了更好地理清系統編程和網絡編程中的一些概念性問題,並無深刻地閱讀分析源碼,我也是草草翻過這本書,請有興趣的朋友本身參考相關資料。此書出版較早,分析的版本爲2.4.16,故出現的一些概念可能跟最新版本內核不一樣。node

此書已經開源,閱讀地址 http://www.kerneltravel.netlinux


1、內存管理概述算法


(一)、虛擬內存實現結構
 
(1)內存映射模塊(mmap):負責把磁盤文件的邏輯地址映射到虛擬地址,以及把虛擬地址映射到物理地址。
 
(2)交換模塊(swap):負責控制內存內容的換入和換出,它經過交換機制,使得在物理內存的頁面(RAM 頁)中保留有效的頁 ,即從主存中淘汰最近沒被訪問的頁,保存近來訪問過的頁。
 
(3)核心內存管理模塊(core):負責核心內存管理功能,即對頁的分配、回收、釋放及請頁處理等,這些功能將被別的內核子系統(如文件系統)使用。
 
(4)結構特定的模塊:負責給各類硬件平臺提供通用接口,這個模塊經過執行命令來改變硬件MMU 的虛擬地址映射,並在發生頁錯誤時,提供了公用的方法來通知別的內核子系統。這個模塊是實現虛擬內存的物理基礎。
 
(二)、內核空間和用戶空間
 
Linux 簡化了分段機制,使得虛擬地址與線性地址老是一致,所以,Linux 的虛擬地址空間也爲0~4G 字節。Linux 內核將這4G 字節的空間分爲兩部分。將最高的1G 字節(從虛擬地址0xC0000000 到0xFFFFFFFF),供內核使用,稱爲「內核空間」。而將較低的3G 字節(從虛擬地址0x00000000 到0xBFFFFFFF),供各個進程使用,稱爲「用戶空間」。由於每一個進程能夠經過系統調用進入內核,所以,Linux 內核由系統內的全部進程共享。因而,從具體進程的角度來看,每一個進程能夠擁有4G 字節的虛擬空間。圖 6.3 給出了進程虛擬空間示意圖。
Linux 使用兩級保護機制:0 級供內核使用,3 級供用戶程序使用。從圖6.3 中能夠看出,每一個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其餘進程是不可見的。最高的1G 字節虛擬內核空間則爲全部進程以及內核所共享。
 
 
(三)、虛擬內存實現機制間的關係
 
首先內存管理程序經過映射機制把用戶程序的邏輯地址映射到物理地址,在用戶程序運行時若是發現程序中要用的虛地址沒有對應的物理內存時,就發出了請頁要求①;若是有空閒的內存可供分配,就請求分配內存②(因而用到了內存的分配和回收),並把正在使用的物理頁記錄在頁緩存中③(使用了緩存機制)。若是沒有足夠的內存可供分配,那麼就調用交換機制,騰出一部份內存④⑤。另外在地址映射中要經過TLB(翻譯後援存儲器)來尋找物理頁⑧;交換機制中也要用到交換緩存⑥,而且把物理頁內容交換到交換文件中後也要修改頁表來映射文件地址⑦。
 
2、內存分配與釋放
 
在Linux 中,CPU 不能按物理地址來訪問存儲空間,而必須使用虛擬地址;所以,對於內存頁面的管理,一般是先在虛存空間中分配一個虛存區間,而後才根據須要爲此區間分配相應的物理頁面並創建起映射,也就是說,虛存區間的分配在前,而物理頁面的分配在後。
 
(一)、夥伴算法(Buddy)
 
Linux 的夥伴算法把全部的空閒頁面分爲10 個塊組,每組中塊的大小是2 的冪次方個頁面,例如,第0 組中塊的大小都爲2^0(1 個頁面),第1 組中塊的大小都爲2^1(2 個頁面),第9 組中塊的大小都爲2^9(512 個頁面)。也就是說,每一組中塊的大小是相同的,且這一樣大小的塊造成一個鏈表。
 
咱們經過一個簡單的例子來講明該算法的工做原理。
 
假設要求分配的塊的大小爲128 個頁面(由多個頁面組成的塊咱們就叫作頁面塊)。該算法先在塊大小爲128 個頁面的鏈表中查找,看是否有這樣一個空閒塊。若是有,就直接分配;若是沒有,該算法會查找下一個更大的塊,具體地說,就是在塊大小256 個頁面的鏈表中查找一個空閒塊。若是存在這樣的空閒塊,內核就把這256 個頁面分爲兩等份,一份分配出去,另外一份插入到塊大小爲128 個頁面的鏈表中。若是在塊大小爲256 個頁面的鏈表中也沒有找到空閒頁塊,就繼續找更大的塊,即512 個頁面的塊。若是存在這樣的塊,內核就從512 個頁面的塊中分出128 個頁面知足請求,而後從384 個頁面中取出256 個頁面插入到塊大小爲256 個頁面的鏈表中。而後把剩餘的128 個頁面插入到塊大小爲128 個頁面的鏈表中。若是512 個頁面的鏈表中尚未空閒塊,該算法就放棄分配,併發出出錯信號。
 
以上過程的逆過程就是塊的釋放過程,這也是該算法名字的來由。知足如下條件的兩個塊稱爲夥伴:
(1)兩個塊的大小相同;
(2)兩個塊的物理地址連續。
 
夥伴算法把知足以上條件的兩個塊合併爲一個塊,該算法是迭代算法,若是合併後的塊還能夠跟相鄰的塊進行合併,那麼該算法就繼續合併。
 
 
(二)、Slab 分配機制
 
 
能夠根據對內存區的使用頻率來對它分類。對於預期頻繁使用的內存區,能夠建立一組特定大小的專用緩衝區進行處理,以免內碎片的產生。對於較少使用的內存區,能夠建立一組通用緩衝區(如Linux 2.0 中所使用的2 的冪次方)來處理,即便這種處理模式產生碎
片,也對整個系統的性能影響不大。
 
硬件高速緩存的使用,又爲儘可能減小對夥伴算法的調用提供了另外一個理由,由於對夥伴算法的每次調用都會「弄髒」硬件高速緩存,所以,這就增長了對內存的平均訪問次數。
 
Slab 分配模式把對象分組放進緩衝區(儘管英文中使用了Cache 這個詞,但實際上指的是內存中的區域,而不是指硬件高速緩存)。由於緩衝區的組織和管理與硬件高速緩存的命中率密切相關,所以,Slab 緩衝區並不是由各個對象直接構成,而是由一連串的「大塊(Slab)」構成,而每一個大塊中則包含了若干個同種類型的對象,這些對象或已被分配,或空閒,如圖6.10 所示。通常而言,對象分兩種,一種是大對象,一種是小對象。所謂小對象,是指在一個頁面中能夠容納下好幾個對象的那種。例如,一個inode 結構大約佔300 多個字節,所以,一個頁面中能夠容納8 個以上的inode 結構,所以,inode 結構就爲小對象。Linux 內核中把小於512 字節的對象叫作小對象。
 
實際上,緩衝區就是主存中的一片區域,把這片區域劃分爲多個塊,每塊就是一個Slab,每一個Slab 由一個或多個頁面組成,每一個Slab 中存放的就是對象。
 
3、地址映射機制
 
在進程的task_struct 結構中包含一個指向 mm_struct 結構的指針,mm_strcut 用來描述一個進程的虛擬地址空間。進程的 mm_struct 則包含裝入的可執行映像信息以及進程的頁目錄指針pgd。該結構還包含有指向 vm_area_struct 結構的幾個指針,每一個 vm_area_struct 表明進程的一個虛擬地址區間。vm_area_struct 結構含有指向vm_operations_struct 結構的一個指針,vm_operations_struct 描述了在這個區間的操做。vm_operations 結構中包含的是函數指針;其中,open、close 分別用於虛擬區間的打開、關閉,而nopage 用於當虛存頁面不在物理內存而引發的「缺頁異常」時所應該調用的函數,當 Linux 處理這一缺頁異常時(請頁機制),就能夠爲新的虛擬內存區分配實際的物理內存。圖6.15 給出了虛擬區間的操做集。
 
 
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 
struct mm_struct
{
    struct vm_area_struct *mmap;            /* list of VMAs */
    struct rb_root mm_rb;
    struct vm_area_struct *mmap_cache;      /* last find_vma result */
    ...
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    ...
};

struct vm_area_struct
{
    struct mm_struct *vm_mm;        /* The address space we belong to. */
    unsigned long vm_start;         /* Our start address within vm_mm. */
    unsigned long vm_end;           /* The first byte after our end address
                                           within vm_mm. */
    ....
    /* linked list of VM areas per task, sorted by address */
    struct vm_area_struct *vm_next;
    ....
    /* describe the permissable operation */
    unsigned long vm_flags;
    /* operations on this area */
    struct vm_operations_struct * vm_ops;
    
    struct file * vm_file; /* File we map to (can be NULL). */
} ;

/*
* These are the virtual MM functions - opening of an area, closing and
* unmapping it (needed to keep files on disk up-to-date etc), pointer
* to the functions called when a no-page or a wp-page exception occurs.
*/
struct vm_operations_struct
{
    void (*open)(struct vm_area_struct *area);
    void (*close)(struct vm_area_struct *area);
    struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int unused);
};
 
 
 
 
 
4、malloc 和 free 的實現
 
 C++ Code 
1
2
3
4
5
6
7
8
9
10
 
Normally,  malloc()  allocates  memory  from  the heap, and adjusts the size of the heap as required, using sbrk(2).  When
allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as  a
private  anonymous mapping using mmap(2).  MMAP_THRESHOLD is 128 kB by default, but is adjustable using mallopt(3).  Allo‐
cations performed using mmap(2) are unaffected by the RLIMIT_DATA resource limit (see getrlimit(2)).

MAP_ANONYMOUS
    The  mapping  is  not  backed  by  any file; its contents are initialized to zero.  The fd and offset arguments are ignored;
    however, some implementations require fd to be - 1 if MAP_ANONYMOUS ( or MAP_ANON) is specified, and  portable applications 
    should ensure this. The use of MAP_ANONYMOUS in conjunction with MAP_SHARED is only supported on Linux since kernel 2.4.
 
(一)、使用brk()/ sbrk() 實現
 
圖中白色背景的框表示 malloc管理的空閒內存塊,深色背景的框不歸 malloc管,多是已經分配給用戶的內存塊,也可能不屬於當前進程, Break之上的地址不屬於當前進程,須要經過 brk系統調用向內核申請。每一個內存塊開頭都有一個頭節點,裏面有一個指針字段和一個長度字段,指針字段把全部空閒塊的頭節點串在一塊兒,組成一個環形鏈表,長度字段記錄着頭節點和後面的內存塊加起來一共有多長,以 8字節爲單位(也就是以頭節點的長度爲單位)。

 

1. 一開始堆空間由一個空閒塊組成,長度爲 7×8=56字節,除頭節點以外的長度爲 48字節。
 
2. 調用 malloc分配 8個字節,要在這個空閒塊的末尾截出 16個字節,其中新的頭節點佔了 8個字節,另外 8個字節返回給用戶使用,注意返回的指針 p1指向頭節點後面的內存塊。
 
3. 又調用 malloc分配 16個字節,又在空閒塊的末尾截出 24個字節,步驟和上一步相似。
 
4. 調用 free釋放 p1所指向的內存塊,內存塊(包括頭節點在內)歸還給了 malloc,如今 malloc管理着兩塊不連續的內存,用環形鏈表串起來。注意這時 p1成了野指針,指向不屬於用戶的內存, p1所指向的內存地址在 Break之下,是屬於當前進程的,因此訪問 p1時不會出現段錯誤,但在訪問 p1時這段內存可能已經被 malloc再次分配出去了,可能會讀到意外改寫數據。另外注意,此時若是經過 p2向右寫越界,有可能覆蓋右邊的頭節點,從而破壞 malloc管理的環形鏈表, malloc就沒法從一個空閒塊的指針字段找到下一個空閒塊了,找到哪去都不必定,全亂套了。
 
5. 調用 malloc分配 16個字節,如今雖然有兩個空閒塊,各有 8個字節可分配,可是這兩塊不連續, malloc只好經過 brk系統調用擡高 Break,得到新的內存空間。在 [K&R]的實現中,每次調用 sbrk函數時申請 1024×8=8192個字節,在 Linux系統上 sbrk函數也是經過 brk實現的,這裏爲了畫圖方便,咱們假設每次調用 sbrk申請 32個字節,創建一個新的空閒塊。
 
6. 新申請的空閒塊和前一個空閒塊連續,所以能夠合併成一個。在能合併時要儘可能合併,以避免空閒塊越割越小,沒法知足大的分配請求。
 
7. 在合併後的這個空閒塊末尾截出 24個字節,新的頭節點佔 8個字節,另外 16個字節返回給用戶。
 
8. 調用 free(p3)釋放這個內存塊,因爲它和前一個空閒塊連續,又從新合併成一個空閒塊。注意, Break只能擡高而不能下降,從內核申請到的內存之後都歸 malloc管了,即便調用 free也不會還給內核。
 
(二)、使用mmap() / munmap() 實現
 

在Linux下面,kernel 使用4096 byte來劃分頁面,而malloc的顆粒度更細,使用8 byte對齊,所以,分配出來的內存不必定是頁對齊的。而mmap 分配出來的內存地址是頁對齊的,因此munmap處理的內存地址必須頁對齊(Page Aligned)。此外,咱們可使用memalign或是posix_memalign來獲取一塊頁對齊的內存。編程

能夠參考《 linux的內存管理模型(上)》這篇文章。
在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這個結構體: 

  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就是基本的劃分單位,即一張一張的撲克牌: 

  1. struct vm_area_struct * mmap;  


這個結構體的定義以下: 

  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函數)時,這個函數內部的一些變量的數值入棧,函數調用完成返回後,局部變量的數值就沒有用了,所以出棧,把內存讓出來給另外一個函數的變量使用(程序在執行時,老是會在某一個函數調用裏面)。 

咱們看一個圖例說明: 

 

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

  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進行實際的內存操做: 

  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的函數定義: 

  1. void *  
  2. mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);  


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

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

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


flags表明用於控制不少的內存屬性,咱們一下子會用到,這裏不展開。 

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

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

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

  1. int  
  2. munmap(void *addr, size_t len);  


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


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

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


由於不管是malloc仍是free,都須要咱們總體提交待分配和銷燬的所有內存。因而天然而然想到,是否能夠malloc分配內存後,而後使用munmap來部分地釋放呢?下面是一個嘗試: 

  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來獲取一塊頁對齊的內存: 

  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,說明執行成功)。仔細看一下被分配出來的地址: 

  1. 0x7fe09b804000  


轉換到10進制是:140602658275328 

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

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

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

  1. mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)  


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

  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在上面打洞。下面是實驗代碼: 

  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。 

我在本文的下篇中,將詳細介紹Kernel Space和User Space的結合編碼,實現更細顆粒度的內存操做。 

參考資料 

Experiments with the Linux Kernel: Process Segments 

How to find the physical address of a variable from user-space in Linux? 

Simplest way to get physical address from the logical one in linux kernel module 

Page Map 

anon_mmap.c 

Mmap 

mmap()--Memory Map a File 

C_dynamic_memory_allocation 

What are the differences between "brk()" and "mmap()"? 

How to guarantee alignment with malloc and or new? 

Understanding Memory Pages and Page Alignment
  • 大小: 23.2 KB
  • 大小: 76.1 KB
  • 大小: 240.1 KB
  • 大小: 28.2 KB
  • 大小: 22.6 KB
  • 大小: 43 KB
  • 大小: 20.7 KB
  • 大小: 20.7 KB
  • 大小: 26.1 KB
相關文章
相關標籤/搜索