Linux下進程記憶體管理之malloc和sbrk
之前自己突發興趣想寫一下malloc函數,順便瞭解一下進程的記憶體管理。在寫的過程中發現其實malloc只不過是通過調用Linux下的sbrk函數來實現記憶體的分配,只是在sbrk之上加了一層對所分配的記憶體的管理罷了,而sbrk以及brk是實現從虛擬記憶體到記憶體的映射的。在實際動手寫之前先來瞭解一下Linux下一個進程的記憶體空間分配。
進程記憶體空間分配
Linux下每個進程所分配的虛擬記憶體空間是3G,但實際使用過程中不可能也沒有必要為一個進程分配如此大的空間,畢竟記憶體是很寶貴的資源。當一個進程執行的時候系統為其分配的記憶體空間主要包括資料區段,程式碼片段,棧,堆等等。而malloc所申請的空間就是從堆中分配的。先來看下面這張圖:
這就是一個進程的記憶體空間,其中的Data Segment出要是存放已經初始化的待用資料,而BSS segment則存放為初始化的待用資料,在此之上的堆,然後是棧。值得注意的是,堆和棧的增長方向正好是相反的。現在先通過一段簡單的代碼來看一下data segment 和BSS segment的分配。
8 #include <stdio.h> 9 #include <stdlib.h> 10 11 int bssvar; 18 int dataSegmentVar = 1; 19 20 int main() 21 { 22 printf("bssvar:%p, dataSegmentVar:%p,gap:%d", &bssvar, &dataSegmentVar, ((int)&bssvar - (int)&dataSegmentVar)); 23 return 0; 24 } 程式啟動並執行結果是:
bssvar:0x6008c0, dataSegmentVar:0x6008ac,gap:20
可以看到dataSegment在BSS segment之下,他們之間的有20個位元組的空間也即data segment的分配空間大小是20位元組。但是這個大小並不是固定的,如果程式中的靜態未初始設定變數大於20個位元組,那麼data segment的空間會相應地增長。
8 #include <stdio.h> 9 #include <stdlib.h> 10 11 int bssvar, bssvar1, bssvar2, bssvar3, bssvar4, bssvar5; 12 char c; 13 int dataSegmentVar = 1; 14 15 int main() 16 { 17 printf("bssvar:%p, dataSegmentVar:%p,gap:%d", &bssvar, &dataSegmentVar, ((int)&bssvar - (int)&dataSegmentVar)); 18 return 0; 19 }程式啟動並執行結果是:
bssvar:0x6008c4, dataSegmentVar:0x6008ac,gap:24
這個時候dataSegment的變數是5個int和一個char,總共的大小是21個位元組,而此時dataSegment的大小是24個位元組,空間超過20,但是為了對齊,所以不是21而是24。
另外在中還有一個值得注意的地方就是program break,這是進程堆的末尾地址。當使用者通過malloc函數申請空間的時候,實際就是利用sbrk函數移動program break,使其向上增長,以獲得更大的堆空間。所以看起來很神秘的記憶體申請只不過是移動一個指標而已,哈哈。
不過這隻是對簡單的原理,裡面還有很多細節需要考慮,接下來還是用一段程式來說。
8 #include <stdio.h> 9 #include <stdlib.h> 10 11 int main() 12 { 13 void* ptr, *ptr1; 14 ptr = sbrk(0); 15 printf("sbrk:%p\n", ptr); 16 ptr1 = malloc(100); 17 ptr = sbrk(0); 18 printf("sbrk:%p, ptr1:%p\n", ptr, ptr1); 19 free(ptr1); 20 ptr = sbrk(0); 21 printf("sbrk:%p\n",ptr); 22 }~
程式中首先用sbrk(0)得到堆部分的末尾地址,然後利用malloc申請了一個100位元組長度的空間,這個時候再來看堆空間的末尾地址以及所申請的空間的地址。最後,再釋放所申請的空間然後再來看堆空間地址。
程式的運行結果:
sbrk:0x2439000
sbrk:0x245a000, ptr1:0x2439010
sbrk:0x245a000
一開始堆區的末尾地址是0x2439000,但是當利用malloc申請完100位元組的空間之後,堆區的末尾地址變為了0x245a000,一下子變大了0x21000。另外還值得注意的就是malloc所申請的空間的起始地址是0x2439010,比一開始的堆末尾地址向後移動了16個位元組。這個不難理解,每一段記憶體空間都需要有一些中繼資料去管理該空間,所以我猜想這16個位元組就是用來記錄malloc所分配這100個位元組空間的資訊,包括大小,狀態等等。
那麼為什麼明明只申請了100個位元組的空間,program break卻向後移動了這麼多?這個也不難理解,總不能每次使用者申請一段小的空間都去調用一次sbrk吧,這樣的開銷太大。所以乾脆一次性分配一段大空間出來,除了使用者所申請的空間之外,剩下的空間可以用於之後的malloc空間申請。來看下一段程式:
8 #include <stdio.h> 9 #include <stdlib.h> 10 11 int main() 12 { 13 void* ptr, *ptr1; 14 ptr = sbrk(0); 15 printf("sbrk:%p\n", ptr); 16 ptr1 = malloc(100); 17 ptr = sbrk(0); 18 printf("sbrk:%p, ptr1:%p\n", ptr, ptr1); 19 ptr1 = malloc(100); 20 ptr = sbrk(0); 21 printf("sbrk:%p, ptr1:%p\n",ptr, ptr1); 22 free(ptr1); 23 ptr = sbrk(0); 24 printf("sbrk:%p\n",ptr); 25 }
運行結果:
sbrk:0x933000
sbrk:0x954000, ptr1:0x933010
sbrk:0x954000, ptr1:0x933080
sbrk:0x954000
可以看到,儘管通過malloc函數申請了兩塊100位元組的空間,但是program break並未因此而移動兩次。另外,第一塊空間和第二塊空間的地址相差的不是100個位元組而是112個位元組,究其原因估計還是因為對齊的問題吧。
通過上文的講解,我們發現,其實malloc也沒有這麼神秘了,它只不過就是利用sbrk來申請了一段空間罷了。不過除了申請空間之外,還需要管理這些空間才是malloc真正核心的地方。這些問題將在下一篇博文《自己動手寫malloc》中詳細講解。