Linux中的內存管理(四)--Heap


前幾次我們分析了Linux系統中用戶進程的4G虛存大致分為了幾個部分,介紹了3G用戶空間中數據段,代碼段等靜態區域的虛存管理,重點分析了棧的使用。這次我們來分析一下虛存使用中另一個重要部分--堆。前面的介紹中,我們知道編譯器,操作系統擔負着大量棧分配管理的工作。不論是靜態分配的棧空間還是用戶動態分配的棧空間,在函數返回的時候就自動釋放了。堆的使用比之棧而言更為靈活,允許程序員動態的分配並釋放,但也意味着,堆的使用需要程序員更為小心。

4.5 堆的內存管理

在學習"數據結構"的時候,我們知道堆,棧都是基本的數據結構。但是在內存管理的時候,雖然我們常常將堆區和棧區放到一起來說,但其實他們在很多方面都存在着不同。棧區內確實是棧數據結構,並且由計算機硬件,操作系統,以及編譯器配合完成,是計算機運行的基本數據結構。在匯編語言中,我們常說的"堆棧",其實就是指的棧。堆區其實指的是在程序運行過程中動態分配的內存區域,它的管理通常在函數庫中完成。之所以叫做堆是因為通常是使用堆這種數據結構來管理分配的內存。換句話說,其實也可以用任何的數據結構來管理,甚至是一個簡單的鏈表。之所以用堆,是因為在速度,空間利用,和可調節性上,堆有着其自己的優勢。

4.5.1 堆管理的相關庫函數

在ISO C中規定了三個動態分配內存的函數,分別是:
        void *malloc(size_t size);
        void *calloc(size_t nmemb, size_t size);
        void *realloc(void *ptr, size_t size);
在這三個庫函數中,大家最常用的就是malloc。調用malloc函數可以分配長度為size的內存空間,內存空間的數據沒有初始化。其返回值就是指向這段被分配空間的指針。calloc和malloc相似,只不過返回的是一個有nmemb個元素的數組,每個元素的大小是size bytes。也就是分配了nmemb*size大小的內存空間,並將空間內的數據都初始化為0。
realloc是一個比較奇妙的函數,它能將ptr指向的內存塊改為size bytes(ptr由先前malloc,calloc,realloc函數返回)。如果size比以前ptr指向的內存塊大,則會增加分配一塊內存,新增的內存塊沒有初始化。如果size比以前的內存小,則會刪除一塊內存。而保留下來的舊內存里的數據則不會有變化。如果ptr==NULL,則realloc等價於malloc函數,而如果size==0,則realloc等價於free(ptr)函數。realloc的返回值要特別注意。realloc的作用,是對ptr指向的內存大小進行重新調整,但是調整之后的內存空間和原來的內存空間可能不是同一內存地址。也就是說ptr指向的內存塊因大小調整被移動了。所以要把realloc返回的地址指針重新賦值給ptr,即:
        ptr = realloc(ptr,size);
   
    free函數是被用於釋放被分配內存的函數:       
        void free(void *ptr);
   
4.5.2 堆管理的相關系統調用

malloc系列函數的實現與Linux中提供的兩個基本調用是分不開的:
    int brk(void *end_data_segment);
    void *sbrk(intptr_t increment);
brk: brk()的作用和它的名字一樣用於打破系統給進程設置的訪存限制,用於設定進程的內存邊界。如前文所述,堆是從虛存低地址向高地址增長。brk()用於設定堆訪存的上限,也就是堆頂。就像是一個蓋子,隨着堆的分配釋放而上下移動。在這個蓋子之下的內存空間,操作系統都認為是合法的。與brk()相關的還有一個sbrk()函數,sbrk()不是系統調用,而是一個庫函數。sbrk(+/-n)意味着將當前訪存的上限增加/減少n個字節。
   
    void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
    int munmap(void *addr, size_t len);
mmap: mmap()的使用較brk()更為靈活,用途也更為廣泛。可以將虛擬內存地址映射到文件,共享內存等,方便用戶以訪存的方式讀寫文件,完成進程間通信。當然映射后虛存地址就變為合法的了。所以在堆分配的時候,常常借用mmap能向進程添加可訪問虛存空間的能力,加之並不需要讀寫文件等別的要求,所以一般用匿名映射(MAP_ANONYMOUS)來完成。munmap與之所做的事情相反,常用以釋放mmap分配的虛存。

4.5.3 堆的內部管理

對於程序員而言,主要是通過malloc/free來使用動態分配的內存。malloc的實現方式有很多,Glibc中使用的是Doug Lea和Wolfram Gloger實現的版本(dlmalloc),此外還有phkmalloc,Solaris上的malloc等。當然你也完全可以自己實現一個簡單的malloc。無論實現版本怎樣malloc包含着兩部分的內容:內存分配和內存管理。

4.5.3.1 堆空間內存分配
   
當malloc()分配內存的時候,首先會先調用上面提到的brk()或者mmap()來向操作系統申請一塊內存。其實也就是讓操作系統知道這塊內存的虛存地址是有效的。在使用這些虛存地址的時候為其分配相應的物理內存,而不是報Segmentation fault.
    ...
    int *l = sbrk(0);
    k=l+1023;
    printf("k=%d,at %p\n",*k,k);
    ...
    運行程序將會拋出:
    Segmentation fault
   
    如果改為:
    ...
    int *l = sbrk(0);
    sbrk(1);
    k=l+1023;
    printf("k=%d,at %p\n",*k,k);
    ...
    程序將正常運行,並輸出:
    k=100,at 0x804affc
   
第一段代碼出錯是因為程序訪問了還沒分配的內存,超過了當前堆的上限。第二段代碼使用了sbrk(1)動態分配了內存,所以訪問就成功了。注意雖然這里sbrk(1),表面上只把當前堆增加了1個字節。但是因為系統的內存分配是以頁為單位的,當前堆實際增加了4KB, 因此對k = l+1023的訪問也是合法的。
   
brk()和mmap()雖然在內存分配的時候用途一樣,但是各有各的優點,每次brk()的虛存空間是連續的,便於合並,重用,並更為節省頁對齊浪費的空間,但是可能形成內存空洞(見下文),適合較小的內存分配。mmap()不會像brk()那樣形成空洞,但不能復用,合並。且開銷和具體的平台相關,並會把分配的內存初始化為0,所以適合大空間的分配。在dlmalloc中,如果malloc分配的內存小於128KB, 使用brk()來增加進程使用的內存。如果分配的內存大於等於128KB,則使用mmap()來分配內存(128KB這個值在不同的平台上是可調的)。
    下面來看一個例子:
    ...
    int *heap_var = malloc(sizeof(int)); //較小的內存塊分配請求
    int *large_var = malloc(256*1024);    //較大的內存塊分配請求
    printf("Address of heap_var (Heap):%p\n",heap_var);
    printf("Address of large_var (Heap):%p\n",large_var);
    ...
    輸出結果為:
    Address of heap_var  (Heap):0x804a008
Address of large_var (Heap):0xb7db2008
   
    如果用strace命令跟蹤,可以發現這段代碼執行了如下的系統調用:
    brk(0x806b000)                          = 0x806b000
    mmap2(NULL, 266240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7db2000
我們可以清楚地看到,對於較小的內存分配,使用了brk()系統調用,對於較大的內存塊分配請求,使用了mmap系統調用。並且我們發現這兩個地址相差較遠,所以堆區常又被分為兩個部分,一個是brk分配的內存,通常位於低地址。另一個是mmap分配的內存,也叫地址映射區,通常位於高地址。當然用不同系統調用分配的內存,也可以混合管理,這取決於具體的實現。

4.5.3.2 堆空間的內存管理

接下來就是要對用brk和mmap分配好內存進行管理了。因為brk(),mmap()是系統調用,如果每次調用malloc動態分配內存都執行一次系統調用,那開銷是比較大的。再者,如果每次申請的內存較小,但是系統分配的內存都是固定大小的倍數(一般是4KB,一頁),這樣就會有大量的浪費。所以malloc一般會實現一個內存堆來管理這些內存,malloc分配的內存都會以若干chunk的方式放到內存堆中。每次用戶調用malloc動態分配內存的時候,malloc會先到內存堆里進行查找,如果內存堆里沒有合適的空閑chunk,再利用brk/malloc系統調用分配一大塊內存,然后把新分配的大塊內存放到內存堆中,並生成一塊合適的chunk塊返回給用戶。當用戶用free釋放chunk的時候,可能並不立即使用系統調用釋放內存,而是將釋放的chunk作為空閑chunk加入內存堆中,和其他的空閑chunk合並,便於下次分配的時候再次使用。
   
一般說來,釋放的chunk如果標記為mmap申請的,則使用munmap釋放。如果是brk申請的,進一步判斷堆頂之下的空閑chunk是否大於128KB,如果是,則使用brk()釋放。如果小於128KB,仍由內存堆維護。這樣對brk()的使用就會有個問題,當brk()釋放的內存塊在堆頂之下,且內存塊到堆頂之間還有未釋放的內存。那么這塊內存的釋放將不會成功,從而形成內存空洞。
   
malloc中為每塊chunk都會分配一個數據結構用於管理,也就是chunk head。chunk head有多大?我們來看看malloc(0)時的情況。
    ...
    int *heap_var = malloc(0);
    int *heap_var1 = malloc(0);
    printf("Address of heap_var: %p\n",heap_var);
    printf("Address of heap_var1: %p\n", heap_var1);
    ...
    這段代碼的輸出為:
    Address of heap_var: 0x804a008
    Address of heap_var1: 0x804a018
兩者指向的位置相差了16個字節,可以看出,對於malloc(0),也會分配16個字節供chunk head使用,即便這個chunk內包含的內存大小為0。而在c99標准中則對malloc(0)的返回未定義。chunk head中記錄的一個很重要的信息就是當前chunk的大小。當malloc一塊chunk的時候,malloc的內存大小就存放在chunk head中,釋放的時候通過地址指針,找到相應塊的chunk_head,從而知道要釋放的chunk大小。這也是為什么我們在malloc的時候需要指定分配內存的大小,而釋放的時候只需要給出釋放內存的地址指針就行了。如果free(p)時的指針不是malloc時得到的,那么malloc就會報Segmentation fault,或者./chunk: free(): invalid pointer。
   
4.5.4 堆物理內存的使用

堆的使用和棧的使用一樣,都是虛存中的概念。堆物理內存的使用和棧也一樣,采用了延遲分配策略。只有當真正使用虛存的時候才分配相應的物理內存。如:
    ...
    int *large_var = malloc(4*1024*1024);  
    free(large_var);
    ...
   
查看/proc/pid/statm,第一列為虛擬內存大小,第二列是進程所使用的物理內存大小,都是以頁面(4k)為單位。
    malloc之前: 342 78 63 1 0 27 0
    malloc之后;1367 86 70 1 0 1052 0
    free之后:  342 85 70 1 0 27 0
可以看到,malloc之后因為large_var沒有被使用,所以雖然虛擬內存增加了1000多個頁面(約4M),但是物理內存只增加了幾個頁面。
   
    如果程序改為:
    ...
    int *large_var = malloc(4*1024*1024);
    memset(large_var,0,4*1024*1024);
    free(large_var);
    ...
   
再次查看/proc/pid/statm,結果為:
    malloc之前:  343 78 63 1 0 28 0
    malloc之后:  1368 1110 70 1 0 1053 0
    free之后:    343 85 70 1 0 28 0
因為用memset使用了分配的內存,所以這次不僅虛存增加了1000多個頁面,物理內存相應也增加了1000多個頁面。

4.5.5 內存泄漏
在堆的使用過程中,一個很重要的問題就是"內存泄漏"。也就是malloc出來的內存,在不使用之后,用戶未能及時調用free釋放。因為虛存沒有釋放,相應的物理內存也沒有釋放,內存泄漏的堆積最終將耗盡系統所有的內存。為了克服內存泄漏問題,Small Pointer, Garbage Collection等技術被大量的研究和使用。但最有效的辦法還是在編寫程序的時候時刻留意這個問題,小心處理每一次malloc操作。但是"內存泄漏"只是運行時問題,當進程結束的時候,操作系統就會收回所有分配給進程的內存。

小結:
1. 無論是堆,還是棧都是對虛存的操作和管理。
2. 系統調用brk()和mmap()用來動態分配虛存空間,也就是表明這些虛存地址是合法的,訪問的時候,系統應為其分配物理內存,而不是報錯。
3. 堆的本質是動態申請的虛存空間。理論上可以用任何方式去管理這塊空間。但數據結構--"堆"是最常用的一種,所以這塊分配的空間常稱為被堆。
4. 和棧不一樣,堆的管理是在用戶函數庫中進行,malloc/free等函數是堆的入口。
5. 每次分配的內存塊大小都會被記錄下來,釋放的時候只需要指定要釋放的內存地址就行了。這就是為什么malloc的時候要指定大小,free的時候不用。
6. 堆和棧一樣,仍然使用了物理內存的延遲分配策略。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM