關鍵詞:program break、brk()、sbrk()、malloc()、free()、cmalloc()、realloc()、alloca()、mallopt()、mallinfo()。
1. 在堆上分配內存
所謂堆是一段長度可變的連續虛擬內存,始於進程的未初始化數據段末尾,隨着內存的分配和釋放而增減。通常將堆的當前內存邊界稱為“program break”。
C的malloc函數族基於brk()和sbrk()。
1.1 調整program break:brk()和sbrk()
改變堆的大小(即分配或釋放內存),其實就像命令內核改變進程的program break位置一樣簡單。
最初,program break正好位於初始化數據段末尾之后。

在program break的位置抬升后,程序可以訪問新分配區域內的任何內存地址,而此時物理內存頁尚未分配。內核會在進程首次試圖訪問這些虛擬內存地址是自動分配新的物理內存頁。
傳統UNIX中提供了兩個操作program break的系統調用:brk()和sbrk()
#include <unistd.h> int brk(void *end_data_segment); Returns 0 on success, or –1 on error void *sbrk(intptr_t increment); Returns previous program break on success, or (void *) –1 on error
sbrk()會將program break設置為參數end_data_segment所指定的位置。由於虛擬內存以頁為單位進行分配,end_data_segment實際會四舍五入到下一個內存頁的邊界處。
當試圖將program break設置為一個低於其初始值(end)的位置時,有可能會導致無法預知的行為。
program break可以設定的精確上限取決於一些列因素,這包括進程中對數據段大小的資源限制,以及內存映射、共享內存段、共享庫的位置。
調用sbrk()將program break在原有地址上增加從參數increment傳入的大小。用於聲明increment的intptr_t類型屬於整數數據類型。
若調用成功,sbrk()返回前一個program break的地址;換言之,如果program break增加,那么返回值是指向這塊新內存起始位置的指針。
調用sbrk(0)將返回program break的當前位置,對其不做改變。
待研究:初始program break和和上圖關系,通過sbrk()和maps對應驗證。
下面用個測試程序簡單驗證malloc()/free()和program break一下關系。
在不停的malloc()/free()之后,記錄program break的值和系統maps中heap的地址范圍。
#include <unistd.h> #include <stdlib.h> #include <stdio.h> void print_program_break_and_maps(pid_t pid) { char cmd[128]; snprintf(cmd, sizeof(cmd), "cat /proc/%d/maps", pid); printf("program break=%10p.\n", sbrk(0)); system(cmd); printf("\n"); sleep(1); } void main(void) { int i; char *ptr0, *ptr1[256], *ptr2[256], *ptr3[256]; pid_t pid; pid = getpid(); print_program_break_and_maps(pid);-----1 ptr0 = malloc(1024*4); print_program_break_and_maps(pid);-----2 free(ptr0); print_program_break_and_maps(pid);-----3 for(i = 0; i < 256; i++) { ptr1[i] = malloc(1024*4); } print_program_break_and_maps(pid);-----4 for(i = 0; i < 256; i++) { ptr2[i] = malloc(1024*4); } print_program_break_and_maps(pid);-----5 for(i = 0; i < 256; i++) { ptr3[i] = malloc(1024*4); } print_program_break_and_maps(pid);-----6 for(i = 0; i < 256; i++) { free(ptr3[i]); }; print_program_break_and_maps(pid);-----7 for(i = 0; i < 256; i++) { free(ptr1[i]); }; print_program_break_and_maps(pid);-----8 for(i = 0; i < 256; i++) { free(ptr2[i]); }; print_program_break_and_maps(pid);-----9 }
下面結合代碼分析一下program break和heap的變化規律。
program break= 0xb000.-----1、此時program break並不是heap的高地址,說明program break和heap高地址並不是一一對應的。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0002c000 rwxp 00000000 00:00 0 [heap] 10000000-1001d000 r-xp 00000000 b3:01 960 /lib/ld-2.28.9000.so 1001d000-1001e000 r--p 0001c000 b3:01 960 /lib/ld-2.28.9000.so 1001e000-1001f000 rw-p 0001d000 b3:01 960 /lib/ld-2.28.9000.so 1001f000-10020000 r-xp 00000000 00:00 0 [vdso] 10020000-10022000 rw-p 00000000 00:00 0 10022000-1014c000 r-xp 00000000 b3:01 952 /lib/libc-2.28.9000.so 1014c000-1014d000 ---p 0012a000 b3:01 952 /lib/libc-2.28.9000.so 1014d000-1014f000 r--p 0012a000 b3:01 952 /lib/libc-2.28.9000.so 1014f000-10150000 rw-p 0012c000 b3:01 952 /lib/libc-2.28.9000.so 10150000-10153000 rw-p 00000000 00:00 0 7fb77000-7fb98000 rwxp 00000000 00:00 0 [stack] program break= 0x2c000.-----2、malloc()調用之后,program break和heap高地址一致。但是malloc()只分配了4KB,0x21000是malloc()預分配的空間。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0002c000 rwxp 00000000 00:00 0 [heap] ... program break= 0x2c000.-----3、在free(4KB)之后,program break並沒有降低,和第1步不一致。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0002c000 rwxp 00000000 00:00 0 [heap] ... program break= 0x113000.------4、實際分配的空間是4KB*256=1MB,但是heap占用的空間是0x108000,多出了0x8000。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-00113000 rwxp 00000000 00:00 0 [heap] ... program break= 0x21b000.-----5、實際malloc()空間時4KB*256*2=2MB,但是heap占用的空間是0x210000,多出了0x10000。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0021b000 rwxp 00000000 00:00 0 [heap] ... program break= 0x323000.-----6、實際malloc()空間是4KB*256*3=3MB,但是heap占用的空間是0x318000,多出了0x18000。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-00323000 rwxp 00000000 00:00 0 [heap] ... program break= 0x22e000.-----7、釋放了ptr3的1MB空間后,實際heap釋放的空間並沒有1MB。Why? 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0022e000 rwxp 00000000 00:00 0 [heap] ... program break= 0x22e000.-----8、釋放的ptr1並不是heap的頭部,所以probram break和heap都沒有變化。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0022e000 rwxp 00000000 00:00 0 [heap] ... program break= 0x2d000.-----9、釋放了ptr2后,heap還保留了0x22000空間。 00008000-00009000 r-xp 00000000 b3:01 1034 /root/sbrk_test 00009000-0000a000 r--p 00000000 b3:01 1034 /root/sbrk_test 0000a000-0000b000 rw-p 00001000 b3:01 1034 /root/sbrk_test 0000b000-0002d000 rwxp 00000000 00:00 0 [heap] ...
所以基本上heap的區間比實際malloc()都要大,因為malloc()管理需要額外開銷。也驗證了1、 program break指向heap高地址;2、sbrk/heap都是以頁面對齊的;
1.2 在堆上分配內存:malloc()和free()
C程序使用malloc函數族在堆上分配和釋放內存。
較之brk()和sbrk(),malloc函數族具備以下優點:
- 屬於C語言標准的一部分。
- 更易於在多線程程序中使用。
- 接口簡單,允許分配小塊內存。
- 允許隨意釋放內存塊,他們被維護於一張空閑內存列表中,在后續內存分配調用時循環使用。
malloc()函數在堆上分配參數size字節大小的內存,並返回指向新分配內存起始位置處的指針,其所分配的內存未經初始化。
#include <stdlib.h> void *malloc(size_t size); Returns pointer to allocated memory on success, or NULL on error
由於malloc()的返回類型為void*,因為可以將其賦給任意類型的C指針。
若無法分配內存(或許是因為已經抵達program break所能達到的地址上限),則malloc()返回NULL,並設置errno以返回錯誤消息。
free()函數釋放ptr參數所指向的內存塊,該參數應該是之前由malloc(),或者后續其他堆內存分配函數之一所返回的地址。
#include <stdlib.h> void free(void *ptr);
一般情況下,free()並不降低program break的位置,而是將這塊內存添加到空閑內存列表中,供后續malloc()函數循環使用。
- 被釋放的內存通常會位於堆的中間,而非堆的頂部,因為降低program break是不可能的。
- 它最大限度地減少了程序必須執行的sbrk()調用次數。
- 在大多數情況下,降低program break的位置不會對那些分配大量內存的程序有多少幫助,因為他們通常傾向於持有已分配內存或是反復釋放和重新分配內存,而非釋放所有內存后再持續運行一段時間。
傳給free()一個空指針,那么函數將什么也不做。
在調用free()后對參數ptr的任何使用,例如將其再次傳遞給free(),將產生錯誤,並可能導致不可預知的結果。
調用free()還是不調用free()
當進程終止時,其占用的所有內存都會返回給操作系統,包括堆中由malloc函數所分配的內存。但是最好能夠在程序中顯式釋放所有的已分配內存,因為:
- 顯式調用free()能使程序在未來修改時更具可讀性和可維護性。
- 如果使用malloc調試庫來查找程序的內存泄漏問題,那么會將任何未經顯式釋放處理的內存報告為內存泄漏。這會使發現真正內存泄漏的工作復雜化。
1.3 malloc()和free()的實現
malloc()實現首先會掃描之前由free()所釋放的空閑內存塊列表,以求找到尺寸大於等於要求的一塊空閑內存。
如果這一內存塊的尺寸正好與要求相當,就把它直接返回給調用者。如果是一塊較大的內存,那么將對其進行分割,在將一塊大小相當的內存返回給調用者的同時,把較小的那塊空閑內存塊保留在空閑列表中。
如果在空閑內存列表中根本找不到足夠大的空閑內存塊,那么malloc()會調用sbrk()已分配更多的內存。為了減少對sbrk()的調用次數,malloc()並未只是嚴格按所需字節數來分配內存,而是以更大幅度(以虛擬內存頁大小數倍)來增加program brak,並將超出部分置於空閑內存列表。
當free()將內存塊置於空閑列表之上時,是如何知曉內存塊大小的?當malloc()分配內存塊時,會額外分配幾個字節來存放記錄這塊內存大小的整數值。該整數位於內存塊的起始處,而實際返回給調用者的內存地址恰好位於這一長度紀錄字之后。

當將內存塊置於空閑內存列表時,free()會使用內存塊本身的空閑來存放鏈表指針,將自身添加到列表中。

C語言允許程序創建指向堆中任意位置的指針,並修改其指向的數據,包括由free()和malloc()函數維護的內存塊長度、指向前一空閑塊和后一空閑塊的指針。這就要求malloc()和free()要遵守一下規則。
- 分配一塊內存后,應當小心謹慎,不要改變這塊內存范圍外的任何內容。
- 釋放同一塊已分配內存超過一次是錯誤的。當兩次釋放同一塊內存時,更常見的后果是導致不可預知的行為。
- 若非經由malloc函數族中函數所返回的指針,絕不能在free()函數中使用。
- 在編寫需要長時間運行的程序時,如果需要反復分配內存,那么應當確保釋放所有已使用完畢的內存。如果不然,堆將穩步增長,直至抵達可用虛擬內存的上限,在此之后分配內存的任何嘗試都將以失敗告終。
待研究:分析glibc,不同size情況下malloc()/free()看實際內存表現(maps)。
malloc調試的工具和庫
glibc提供的malloc調試功能:
mtrace()和muntrace()函數分別在程序中打開和關閉對內存分配調用進行跟蹤的功能。這些函數與環境變量MALLOC_TRACE搭配使用,該變量定義了跟蹤信息的文件名。
mcheck()和mprobe()函數允許程序對已分配內存塊進行一致性檢查。當程序試圖在已分配內存之外進行寫操作時,他們將捕獲這個錯誤。使用這些函數的程序,必須使用cc-lmcheck選項和mcheck庫連接。
MALLOC_CHECK_環境變量提供了類似mcheck()和mprobe()函數的功能。設置此變量為不同整數值:0,忽略錯誤;1,在標准錯誤輸出中打印診斷錯誤;2,調用abort()來終止程序。
詳細介紹參見:《glibc提供的malloc()的調試工具》
控制和檢測malloc函數包
下面函數用於監測和控制malloc函數族的內存分配:
- 函數mallopt()能修改各項參數,以控制malloc()所采用的算法。
- mallinfo()函數返回一個結構,其中包含由malloc()分配內存的各種統計數據。
待研究:mallopt()對malloc()的影響。
1.4 在堆上分配內存的其他方法
用calloc()和realloc()分配內存
calloc()用於給一組相同對象分配內存:
#include <stdlib.h> void *calloc(size_t numitems, size_t size); Returns pointer to allocated memory on success, or NULL on error
與malloc()不同calloc()會將分配的內存初始化為0.
realloc()函數用來調整一塊內存的大小,此內存塊應是之前由malloc()函數族函數所分配。
#include <stdlib.h> void *realloc(void *ptr, size_t size); Returns pointer to allocated memory on success, or NULL on error
ptr指向需要調整大小的內存塊的指針,size指定所需調整大小的期望值。
如果成功,realloc()返回指向大小調整后內存塊的指針。如發生錯誤,realloc()返回NULL,對ptr指針指向的內存塊則原封不動。
若realloc()增加了已分配內存塊的大小,則不會對額外分配的字節進行初始化。
realloc()增大分配內存時幾種情況
當realloc()增大已分配內存時,會試圖去合並在空閑列表中緊隨其后且大小滿足要求的內存塊。
若原內存快位於堆的頂部,那么realloc()將對對空間進行擴展。
如果內存位於堆中部,且緊臨其后的空閑內存空間大小不足,realloc()會分配一塊新內存,並將所有數據復制到新內存塊中。
由於realloc()可能會移動內存塊,任何指向該內存塊內部的指針在調用realloc()之后都可能不再可用。
分配對齊的內存:memalign()和posix_memalign()
memalign()和posix_memalign()目的在於分配內存時,起始地址要與2的整數次冪邊界對齊。
#include <malloc.h> void *memalign(size_t boundary, size_t size); Returns pointer to allocated memory on success, or NULL on error
boundary必須是2的整數次冪,起始地址是參數boundary的整數倍。
函數返回已分配內存的地址。
#include <stdlib.h> int posix_memalign(void **memptr, size_t alignment, size_t size); Returns 0 on success, or a positive error number on error
內存與alignment參數的整數倍對齊,alignment必須是sizeof(void*)與2的整數次冪兩者間的乘積。
2. 在棧上分配內存:alloca()
alloca()也可以動態分配內存,不過是從棧中分配。當前調用函數的棧幀位於棧的頂部,因此幀的上方存在擴展空間,只需修改棧指針值即可。
#include <alloca.h> void *alloca(size_t size); Returns pointer to allocated block of memory
不需要也絕不可能調用free()來釋放由alloca()分配的內存,也不能用realloc()來調整大小。
alloca()的劣勢
- 若調用alloca()造成堆棧溢出,則程序的行為無法預知,特別是在沒有收到一個NULL返回值通知錯誤的情況下。
- 不能在一個函數的參數列表中調用alloca()。這會使alloca()分配的占空間出現在當前函數參數的空間內,因為函數參數都位於棧內固定位置。
alloca()相對於malloc()的優勢
- alloca()分配內存的速度要快於malloc(),因為編譯器將alloca()作為內聯代碼處理,並且通過直接調整棧指針來實現。alloca()也不需要維護空閑內存列表。
- alloca()分配的內存隨棧幀的溢出而自動釋放,亦即當調用alloca的函數返回之時自動釋放。
- 在信號處理程序中調用longjmp()后siglongjmp()一致性非局部跳轉時,在起跳函數和落地函數之間的函數中,如果使用malloc()來分配內存,要想避免內存泄漏極其困難,如果使用alloca()則完全可以避免這一問題。
