在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這個結構體:
- struct mm_struct {
- struct vm_area_struct * mmap;
- struct rb_root mm_rb;
- struct vm_area_struct * mmap_cache;
- ...
- unsigned long start_code, end_code, start_data, end_data;
- unsigned long start_brk, brk, start_stack;
- ...
- };
mm_struct負責描述進程的內存。至關於發牌人記錄給誰發了哪些牌,發了多少張,等等。那麼,內存是如何將內存進行劃分的呢?也就是說,發牌人手裏假設是一大張未裁剪的撲克紙,他是怎樣將其剪成一張一張的撲克牌呢?上面的vm_area_struct就是基本的劃分單位,即一張一張的撲克牌:
- struct vm_area_struct * mmap;
這個結構體的定義以下:
- struct vm_area_struct {
- struct mm_struct * vm_mm;
- unsigned long vm_start;
- unsigned long vm_end;
- ....
-
- struct vm_area_struct *vm_next;
- ....
- }
這樣,內核就能夠記錄分配給用戶空間的內存了。
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函數)時,這個函數內部的一些變量的數值入棧,函數調用完成返回後,局部變量的數值就沒有用了,所以出棧,把內存讓出來給另外一個函數的變量使用(程序在執行時,老是會在某一個函數調用裏面)。
咱們看一個圖例說明:
爲了更好的理解內存分段,能夠撰寫一段代碼:
- #include <stdio.h>
-
- int global_var;
-
- int global_initialized_var = 5;
-
- void function() {
- int stack_var;
-
-
-
- printf("the function's stack_var is at address 0x%08x\n", &stack_var);
- }
-
-
- int main() {
- int stack_var;
-
-
- static int static_initialized_var = 5;
-
-
- static int static_var;
-
- int *heap_var_ptr;
-
-
-
-
- heap_var_ptr = (int *) malloc(4);
-
-
-
- printf("====IN DATA SEGMENT====\n");
- printf("global_initialized_var is at address 0x%08x\n", &global_initialized_var);
- printf("static_initialized_var is at address 0x%08x\n\n", &static_initialized_var);
-
-
-
- printf("====IN BSS SEGMENT====\n");
- printf("static_var is at address 0x%08x\n", &static_var);
- printf("global_var is at address 0x%08x\n\n", &global_var);
-
-
-
- printf("====IN HEAP====\n");
- printf("heap_var is at address 0x%08x\n\n", heap_var_ptr);
-
-
-
- printf("====IN STACK====\n");
- printf("the main's stack_var is at address 0x%08x\n", &stack_var);
- function();
- }
編譯這個代碼,看看執行結果:
理解了進程的內存空間使用,咱們如今能夠想一想,這幾塊內存當中,最靈活的是哪一塊?沒錯,是Heap。其它幾塊都由C編譯器編譯代碼時預處理,相對固定,而heap內存能夠由malloc和free進行動態的分配和銷燬。
有關malloc和free的使用方法,在本文中我就再也不多說,這些屬於基本知識。咱們在這篇文章中要關心的是,malloc是如何工做的?實際上,它會去調用mmap(),而mmap()則會調用內核,獲取VMA,即前文中看到的vm_area。這一塊工做由c庫向kernel發起請求,而由kernel完成這個請求,在kernel當中,有vm_operations_struct進行實際的內存操做:
- struct vm_operations_struct {
- void (*open)(struct vm_area_struct * area);
- void (*close)(struct vm_area_struct * area);
- ...
- };
能夠看到,kernel能夠對VMA進行open和close,即收發牌的工做。理解了malloc的工做原理,free也不難了,它向下調用munmap()。
下面是mmap和munmap的函數定義:
- void *
- mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
這裏面,addr是但願可以分配到的虛地址,好比:我但願獲得一張牌,作爲我手裏編號爲2的那張。須要注意的是,mmap最後分配出來的內存地址不必定是你想要的,可能你請求一張編號爲2的撲克,但發牌人控制這個編號過程,他會給你一張在你手裏編號爲3的撲克。
prot表明對進程對這塊內存的權限:
- PROT_READ 是否可讀
- PROT_WRITE 是否可寫
- PROT_EXEC IP指針是否能夠指向這裏進行代碼的執行
- PROT_NONE 不能訪問
flags表明用於控制不少的內存屬性,咱們一下子會用到,這裏不展開。
fd是文件描述符。咱們這裏必須明白一個基本原理,任何硬盤上面的數據,都要讀取到內存當中,才能被程序使用,所以,mmap的目的就是將文件數據映射進內存。所以,要在這裏填寫文件描述符。若是你在這裏寫-1,則不映射任何文件數據,只是在內存裏面要上這一塊空間,這就是malloc對mmap的使用方法。
offset是文件的偏移量,好比:從第二行開始映射。文件映射,不是這篇文章關心的內容,不展開。
okay,瞭解了mmap的用法,下面看看munmap:
- int
- munmap(void *addr, size_t len);
munmap很簡單,告訴它要還回去的內存地址(即哪張牌),而後告訴它還回去的數量(多少張),其實更準確的說:尺寸。
如今讓咱們回到題目上來,如何部分地回收一個數組中的內存?咱們知道,使用malloc和free是沒法完成的:
- #include <stdlib.h>
- int main() {
- int *p = malloc(12);
- free(p);
- return 0;
- }
由於不管是malloc仍是free,都須要咱們總體提交待分配和銷燬的所有內存。因而天然而然想到,是否能夠malloc分配內存後,而後使用munmap來部分地釋放呢?下面是一個嘗試:
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
-
- int main() {
- int *arr;
- int *p;
- p = arr = (int*) malloc(3 * sizeof(int));
- int i = 0;
-
- for (i=0;i<3;i++) {
- *p = i;
- printf("address of arr[%d]: %p\n", i, p);
- p++;
- }
-
- printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));
- }
運行這段代碼輸出以下:
注意到munmap調用返回-1,說明內存釋放未成功,這是因爲munmap處理的內存地址必須頁對齊(Page Aligned)。在Linux下面,kernel使用4096 byte來劃分頁面,而malloc的顆粒度更細,使用8 byte對齊,所以,分配出來的內存不必定是頁對齊的。爲了解決這個問題,咱們可使用memalign或是posix_memalign來獲取一塊頁對齊的內存:
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
-
- int main() {
- void *arr;
- printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 4096));
- printf("address of arr: %p\n", arr);
- printf("munmap: %d\n", munmap(arr, 4096));
- }
運行上述代碼得結果以下:
能夠看到,頁對齊的內存資源能夠被munmap正確處理(munmap返回值爲0,說明執行成功)。仔細看一下被分配出來的地址:
轉換到10進制是:140602658275328
試試看是否能被4096整除:140602658275328 / 4096 = 34326820868
能夠被整除,驗證了分配出來的地址是頁對齊的。
接下來,咱們試用一下mmap,來分配一塊內存空間:
- mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0)
注意上面mmap的使用方法。其中,咱們不指定虛地址,讓內核決定內存地址,也就是說,咱們要是要一張牌,但不關心給牌編什麼號。而後PROT_READ|PROT_WRITE表示這塊內存可讀寫,接下來注意flags裏面有MAP_ANONYMOUS,表示這塊內存不用於映射文件。下面是完整代碼:
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
-
- int main() {
- int *arr;
- int *p;
- p = arr = (int*) mmap(NULL, 3 * sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
- int i = 0;
-
- for (i=0;i<3;i++) {
- *p = i;
- printf("address of arr[%d]: %p\n", i, p);
- p++;
- }
-
- printf("munmap: %d\n", munmap(arr, 3 * sizeof(int)));
- }
運行結果以下:
注意munmap返回值爲0,說明內存釋放成功了。所以,驗證了mmap分配出來的內存是頁對齊的。
okay,瞭解了全部這些背景知識,咱們如今應該對給內存打洞這個問題有一個思路了。咱們能夠建立以Page爲基本單元的內存空間,而後用munmap在上面打洞。下面是實驗代碼:
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
-
- int main() {
- void *arr;
- printf("posix_memalign: %d\n", posix_memalign(&arr, 4096, 3 * 4096));
- printf("address of arr: %p\n", arr);
- printf("address of arr[4096]: %p\n", &arr[4096]);
- printf("munmap: %d\n", munmap(&arr[4096], 4096));
- }
咱們申請了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