一提到內(nèi)存管理,我們頭腦中閃出的兩個概念,就是虛擬內(nèi)存,與物理內(nèi)存。這兩個概念主要來自于linux內(nèi)核的支持。
Linux在內(nèi)存管理上份為兩級,一級是線性區(qū),類似于00c73000-00c88000,對應(yīng)于虛擬內(nèi)存,它實際上不占用實際物理內(nèi)存;一級是具體的物理頁面,它對應(yīng)我們機器上的物理內(nèi)存。
這里要提到一個很重要的概念,內(nèi)存的延遲分配。Linux內(nèi)核在用戶申請內(nèi)存的時候,只是給它分配了一個線性區(qū)(也就是虛存),并沒有分配實際物理內(nèi)存;只有當(dāng)用戶使用這塊內(nèi)存的時候,內(nèi)核才會分配具體的物理頁面給用戶,這時候才占用寶貴的物理內(nèi)存。內(nèi)核釋放物理頁面是通過釋放線性區(qū),找到其所對應(yīng)的物理頁面,將其全部釋放的過程。
[cpp]?view plain?copy
print?
char?*p=malloc(2048)?//這里只是分配了虛擬內(nèi)存2048,并不占用實際內(nèi)存。??
strcpy(p,”123”)?//分配了物理頁面,雖然只是使用了3個字節(jié),但內(nèi)存還是為它分配了2048字節(jié)的物理內(nèi)存。??
free(p)?//通過虛擬地址,找到其所對應(yīng)的物理頁面,釋放物理頁面,釋放線性區(qū)。??
我們知道用戶的進程和內(nèi)核是運行在不同的級別,進程與內(nèi)核之間的通訊是通過系統(tǒng)調(diào)用來完成的。進程在申請和釋放內(nèi)存,主要通過brk,sbrk,mmap,unmmap這幾個系統(tǒng)調(diào)用,傳遞的參數(shù)主要是對應(yīng)的虛擬內(nèi)存。
注意一點,在進程只能訪問虛擬內(nèi)存,它實際上是看不到內(nèi)核物理內(nèi)存的使用,這對于進程是完全透明的。
glibc內(nèi)存管理器
那么我們每次調(diào)用malloc來分配一塊內(nèi)存,都進行相應(yīng)的系統(tǒng)調(diào)用呢?
答案是否定的,這里我要引入一個新的概念,glibc的內(nèi)存管理器。
我們知道m(xù)alloc和free等函數(shù)都是包含在glibc庫里面的庫函數(shù),我們試想一下,每做一次內(nèi)存操作,都要調(diào)用系統(tǒng)調(diào)用的話,那么程序?qū)⒍嗝吹牡托А?/p>
實際上glibc采用了一種批發(fā)和零售的方式來管理內(nèi)存。glibc每次通過系統(tǒng)調(diào)用的方式申請一大塊內(nèi)存(虛擬內(nèi)存),當(dāng)進程申請內(nèi)存時,glibc就從自己獲得的內(nèi)存中取出一塊給進程。
內(nèi)存管理器面臨的困難
我們在寫程序的時候,每次申請的內(nèi)存塊大小不規(guī)律,而且存在頻繁的申請和釋放,這樣不可避免的就會產(chǎn)生內(nèi)存碎塊。而內(nèi)存碎塊,直接會導(dǎo)致大塊內(nèi)存申請無法滿足,從而更多的占用系統(tǒng)資源;如果進行碎塊整理的話,又會增加cpu的負荷,很多都是互相矛盾的指標,這里我就不細說了。
我們在寫程序時,涉及內(nèi)存時,有兩個概念heap和stack。傳統(tǒng)的說法stack的內(nèi)存地址是向下增長的,heap的內(nèi)存地址是向上增長的。
函數(shù)malloc和free,主要是針對heap進行操作,由程序員自主控制內(nèi)存的訪問。
在這里heap的內(nèi)存地址向上增長,這句話不完全正確。
glibc對于heap內(nèi)存申請大于128k的內(nèi)存申請,glibc采用mmap的方式向內(nèi)核申請內(nèi)存,這不能保證內(nèi)存地址向上增長;小于128k的則采用brk,對于它來講是正確的。128k的閥值,可以通過glibc的庫函數(shù)進行設(shè)置。
這里我先講大塊內(nèi)存的申請,也即對應(yīng)于mmap系統(tǒng)調(diào)用。
對于大塊內(nèi)存申請,glibc直接使用mmap系統(tǒng)調(diào)用為其劃分出另一塊虛擬地址,供進程單獨使用;在該塊內(nèi)存釋放時,使用unmmap系統(tǒng)調(diào)用將這塊內(nèi)存釋放,這個過程中間不會產(chǎn)生內(nèi)存碎塊等問題。
針對小塊內(nèi)存的申請,在程序啟動之后,進程會獲得一個heap底端的地址,進程每次進行內(nèi)存申請時,glibc會將堆頂向上增長來擴展內(nèi)存空間,也就是我們所說的堆地址向上增長。在對這些小塊內(nèi)存進行操作時,便會產(chǎn)生內(nèi)存碎塊的問題。實際上brk和sbrk系統(tǒng)調(diào)用,就是調(diào)整heap頂?shù)刂分羔槨?/p>
那么heap堆的內(nèi)存是什么時候釋放呢?
當(dāng)glibc發(fā)現(xiàn)堆頂有連續(xù)的128k的空間是空閑的時候,它就會通過brk或sbrk系統(tǒng)調(diào)用,來調(diào)整heap頂?shù)奈恢?,將占用的?nèi)存返回給系統(tǒng)。這時,內(nèi)核會通過刪除相應(yīng)的線性區(qū),來釋放占用的物理內(nèi)存。
下面我要講一個內(nèi)存空洞的問題:
一個場景,堆頂有一塊正在使用的內(nèi)存,而下面有很大的連續(xù)內(nèi)存已經(jīng)被釋放掉了,那么這塊內(nèi)存是否能夠被釋放?其對應(yīng)的物理內(nèi)存是否能夠被釋放?
很遺憾,不能。
這也就是說,只要堆頂?shù)牟糠稚暾垉?nèi)存還在占用,我在下面釋放的內(nèi)存再多,都不會被返回到系統(tǒng)中,仍然占用著物理內(nèi)存。為什么會這樣呢?
這主要是與內(nèi)核在處理堆的時候,過于簡單,它只能通過調(diào)整堆頂指針的方式來調(diào)整調(diào)整程序占用的線性區(qū);而又只能通過調(diào)整線性區(qū)的方式,來釋放內(nèi)存。所以只要堆頂不減小,占用的內(nèi)存就不會釋放。
提一個問題:
1
2
char *p=malloc(2);
free(p)
為什么申請內(nèi)存的時候,需要兩個參數(shù),一個是內(nèi)存大小,一個是返回的指針;而釋放內(nèi)存的時候,卻只要內(nèi)存的指針呢?
這主要是和glibc的內(nèi)存管理機制有關(guān)。glibc中,為每一塊內(nèi)存維護了一個chunk的結(jié)構(gòu)。glibc在分配內(nèi)存時,glibc先填寫chunk結(jié)構(gòu)中內(nèi)存塊的大小,然后是分配給進程的內(nèi)存。
1
2
chunk ------size
p------------ content
在進程釋放內(nèi)存時,只要 指針-4 便可以找到該塊內(nèi)存的大小,從而釋放掉。
注:glibc在做內(nèi)存申請時,最少分配16個字節(jié),以便能夠維護chunk結(jié)構(gòu)。
glibc提供的調(diào)試工具:
為了方便調(diào)試,glibc 為用戶提供了 malloc 等等函數(shù)的鉤子(hook),如 __malloc_hook
對應(yīng)的是一個函數(shù)指針,
1
void *function (size_t size, const void *caller)
其中 caller 是調(diào)用 malloc 返回值的接受者(一個指針的地址)。另外有 __malloc_initialize_hook函數(shù)指針,僅僅會調(diào)用一次(第一次分配動態(tài)內(nèi)存時)。(malloc.h)
一些使用 malloc 的統(tǒng)計量(SVID 擴展)可以用 struct mallinfo 儲存,可調(diào)用獲得。
1
struct mallinfo mallinfo (void)
如何檢測 memory leakage?glibc 提供了一個函數(shù)
void mtrace (void)及其反作用void muntrace (void)
這時會依賴于一個環(huán)境變量 MALLOC_TRACE 所指的文件,把一些信息記錄在該文件中
用于偵測 memory leakage,其本質(zhì)是安裝了前面提到的 hook。一般將這些函數(shù)用
#ifdef DEBUGGING 包裹以便在非調(diào)試態(tài)下減少開銷。產(chǎn)生的文件據(jù)說不建議自己去讀,
而使用 mtrace 程序(perl 腳本來進行分析)。下面用一個簡單的例子說明這個過程,這是
源程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
#include
#include
intmain( int argc, char *argv[] )
{
int *p, *q ;
#ifdef DEBUGGING
mtrace( ) ;
#endif
p = malloc( sizeof( int ) ) ;
q = malloc( sizeof( int ) ) ;
printf( "p = %p\nq = %p\n", p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return 0 ;
}
很簡單的程序,其中 q 沒有被釋放。我們設(shè)置了環(huán)境變量后并且 touch 出該文件
執(zhí)行結(jié)果如下:
p = 0x98c0378q = 0x98c0388
該文件內(nèi)容如下
1
2
3
4
= Start
@./test30:[0x8048446] + 0x98c0378 0x4
@./test30:[0x8048455] + 0x98c0388 0x4
@./test30:[0x804848f] - 0x98c0378
到這里我基本上講完了,我們寫程序時,數(shù)據(jù)部分內(nèi)存使用的問題。
代碼占用的內(nèi)存
數(shù)據(jù)部分占用內(nèi)存,那么我們寫的程序是不是也占用內(nèi)存呢?
在linux中,程序的加載,涉及到兩個工具,linker 和loader。Linker主要涉及動態(tài)鏈接庫的使用,loader主要涉及軟件的加載。
exec執(zhí)行一個程序
elf為現(xiàn)在非常流行的可執(zhí)行文件的格式,它為程序運行劃分了兩個段,一個段是可以執(zhí)行的代碼段,它是只讀,可執(zhí)行;另一個段是數(shù)據(jù)段,它是可讀寫,不能執(zhí)行。
loader會啟動,通過mmap系統(tǒng)調(diào)用,將代碼端和數(shù)據(jù)段映射到內(nèi)存中,其實也就是為其分配了虛擬內(nèi)存,注意這時候,還不占用物理內(nèi)存;只有程序執(zhí)行到了相應(yīng)的地方,內(nèi)核才會為其分配物理內(nèi)存。
loader會去查找該程序依賴的鏈接庫,首先看該鏈接庫是否被映射進內(nèi)存中,如果沒有使用mmap,將代碼段與數(shù)據(jù)段映射到內(nèi)存中,否則只是將其加入進程的地址空間。這樣比如glibc等庫的內(nèi)存地址空間是完全一樣。
因此一個2M的程序,執(zhí)行時,并不意味著為其分配了2M的物理內(nèi)存,這與其運行了的代碼量,與其所依賴的動態(tài)鏈接庫有關(guān)。
運行過程中鏈接動態(tài)鏈接庫與編譯過程中鏈接動態(tài)庫的區(qū)別
我們調(diào)用動態(tài)鏈接庫有兩種方法:一種是編譯的時候,指明所依賴的動態(tài)鏈接庫,這樣loader可以在程序啟動的時候,來所有的動態(tài)鏈接映射到內(nèi)存中;一種是在運行過程中,通過dlopen和dlfree的方式加載動態(tài)鏈接庫,動態(tài)將動態(tài)鏈接庫加載到內(nèi)存中。
這兩種方式,從編程角度來講,第一種是最方便的,效率上影響也不大,在內(nèi)存使用上有些差別。
第一種方式,一個庫的代碼,只要運行過一次,便會占用物理內(nèi)存,之后即使再也不使用,也會占用物理內(nèi)存,直到進程的終止。
第二中方式,庫代碼占用的內(nèi)存,可以通過dlfree的方式,釋放掉,返回給物理內(nèi)存。
這個差別主要對于那些壽命很長,但又會偶爾調(diào)用各種庫的進程有關(guān)。如果是這類進程,建議采用第二種方式調(diào)用動態(tài)鏈接庫。
占用內(nèi)存的測量
測量一個進程占用了多少內(nèi)存,linux為我們提供了一個很方便的方法,/proc目錄為我們提供了所有的信息,實際上top等工具也通過這里來獲取相應(yīng)的信息。
1
2
3
4
5
/proc/meminfo 機器的內(nèi)存使用信息
/proc/pid/maps pid為進程號,顯示當(dāng)前進程所占用的虛擬地址。
/proc/pid/statm 進程所占用的內(nèi)存
[root@localhost ~]# cat /proc/self/statm
654 57 44 0 0 334 0
輸出解釋
CPU 以及CPU0。。。的每行的每個參數(shù)意思(以第一行為例)為:
參數(shù) 解釋 /proc//status
1
2
3
4
5
6
7
Size (pages) 任務(wù)虛擬地址空間的大小 VmSize/4
Resident(pages) 應(yīng)用程序正在使用的物理內(nèi)存的大小 VmRSS/4
Shared(pages) 共享頁數(shù) 0
Trs(pages) 程序所擁有的可執(zhí)行虛擬內(nèi)存的大小 VmExe/4
Lrs(pages) 被映像到任務(wù)的虛擬內(nèi)存空間的庫的大小 VmLib/4
Drs(pages) 程序數(shù)據(jù)段和用戶態(tài)的棧的大小 (VmData+ VmStk )4
dt(pages) 04
查看機器可用內(nèi)存
1
2
3
4
5
/proc/28248/>free
total used free shared buffers cached
Mem: 1023788 926400 97388 0 134668 503688
-/+ buffers/cache: 288044 735744
Swap: 1959920 89608 1870312
我們通過free命令查看機器空閑內(nèi)存時,會發(fā)現(xiàn)free的值很小。這主要是因為,在linux中有這么一種思想,內(nèi)存不用白不用,因此它盡可能的cache和buffer一些數(shù)據(jù),以方便下次使用。但實際上這些內(nèi)存也是可以立刻拿來使用的。
所以 空閑內(nèi)存=free+buffers+cached=total-used
查看進程使用的內(nèi)存
查看一個進程使用的內(nèi)存,是一個很令人困惑的事情。因為我們寫的程序,必然要用到動態(tài)鏈接庫,將其加入到自己的地址空間中,但是/proc/pid/statm統(tǒng)計出來的數(shù)據(jù),會將這些動態(tài)鏈接庫所占用的內(nèi)存也簡單的算進來。
這樣帶來的問題,動態(tài)鏈接庫占用的內(nèi)存有些是其他程序使用時占用的,卻算在了你這里。你的程序中包含了子進程,那么有些動態(tài)鏈接庫重用的內(nèi)存會被重復(fù)計算。
因此要想準確的評估一個程序所占用的內(nèi)存是十分困難的,通過寫一個module的方式,來準確計算某一段虛擬地址所占用的內(nèi)存,可能對我們有用。
?
評論
查看更多