2017年末锻弓,手Q春節(jié)紅包項目期間辣辫,為保障活動期間服務(wù)正常穩(wěn)定,我對性能不佳的Ark Server進行了改造和重寫忧便。重編發(fā)布一段時間后族吻,結(jié)果發(fā)現(xiàn)新發(fā)布的Svr的機器內(nèi)存一直在上漲。如下圖示:
觀察后珠增,第一反應(yīng)是完了超歌,一定存在內(nèi)存泄漏〉俳蹋花了3巍举、4天時間,使用各種辦法進行定位凝垛,一無所獲懊悯。
后來無意中在SPP日志中發(fā)現(xiàn)了端倪简烘,日志中一直打印
tcp socket[%d] user check pkg not ok, but no more buff
,看代碼邏輯定枷,是收包緩沖區(qū)太小,導(dǎo)致調(diào)用方不斷使用new
操作來擴充緩沖區(qū)届氢。我仔細檢查了下調(diào)用方的代碼邏輯欠窒,使用的是SPP微線程架構(gòu),收包緩沖區(qū)是一個Msg的局部變量退子,在Msg析構(gòu)時岖妄,都會調(diào)用delete
,換而言之寂祥,這里絕不可能存在內(nèi)存泄漏荐虐。既然不存在內(nèi)存泄漏,內(nèi)存為什么會一直漲呢丸凭?按照上圖來看福扬,內(nèi)存在1天內(nèi)漲了1G左右,這個速度也太可怕了吧惜犀。既然唯一的線索在內(nèi)存分配操作
new
和delete
上铛碑,那么只可能是這里有貓膩。網(wǎng)上搜索了下
delete not return memory
虽界,果然說來話長啊汽烦。下面我們就來回顧下C++程序中的內(nèi)存管理機制........
物理內(nèi)存、虛擬內(nèi)存
首先莉御,要理清楚2個概念:虛擬內(nèi)存(空間)撇吞、物理內(nèi)存
物理內(nèi)存好說,就是機器的真實內(nèi)存礁叔,你機器是多大內(nèi)存條牍颈,物理內(nèi)存就多大。虛擬內(nèi)存(虛擬地址空間)是一個邏輯概念晴圾,32bit下每個進程都有4G虛擬地址空間颂砸,而且每個進程間的地址空間相互獨立。
從進程的角度來說死姚,每個進程均認為自己獨享整個內(nèi)存空間(4G)人乓。進程空間分布如下圖:
如上圖示:最高的1G空間保留給內(nèi)核使用。接下來是棧都毒,棧向低地址方向延伸(棧的大小受
RLIMIT_STACK
限制色罚,默認為8M),下面是MMAP區(qū)(文件映射內(nèi)存账劲,如動態(tài)庫等戳护,SPP微線程的私有棧也位于這里)金抡,下面是堆(動態(tài)內(nèi)存增長),堆向高地址方向延伸腌且,接下來依次是BSS梗肝、數(shù)據(jù)段、代碼段铺董。
需要注意的一點是:上面所說的都是虛擬內(nèi)存巫击。只有在真正使用到這片內(nèi)存空間時,才會涉及到物理內(nèi)存頁的的分配等(內(nèi)核管理精续,頁錯誤)坝锰。
Linux下動態(tài)內(nèi)存分配實現(xiàn)機制
C、C++的動態(tài)內(nèi)存分配重付、管理都是基于malloc和free的顷级,動態(tài)內(nèi)存即虛擬空間堆區(qū)。另外多說一句确垫,malloc和free操作的也是虛擬地址空間弓颈。
malloc,動態(tài)內(nèi)存分配函數(shù)森爽。是通過brk(sbrk)
和mmap
這兩個系統(tǒng)調(diào)用實現(xiàn)的恨豁。
結(jié)合上文進程虛擬空間圖,brk(sbrk)是將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址推爬迟。mmap是在進程的虛擬地址空間中(堆和棧中間橘蜜,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存。這兩種實現(xiàn)方式的區(qū)別大致如下:
- brk(sbrk)付呕,性能損耗少; mmap相對而言计福,性能損耗大
- mmap不存在內(nèi)存碎片(是物理頁對齊的,整頁映射和釋放); brk(sbrk)可能存在內(nèi)存碎片(由于new和delete的順序不同徽职,可能存在空洞象颖,又稱為碎片)
無論是通過brk(sbrk)還是mmap調(diào)用分配的內(nèi)存都是虛擬空間的內(nèi)存,只有在第一次訪問已分配的虛擬地址空間的時候姆钉,發(fā)生缺頁中斷说订,操作系統(tǒng)負責分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系潮瓶。
delete陶冷,動態(tài)內(nèi)存釋放函數(shù)。如果是brk(sbrk)分配的內(nèi)存毯辅,直接調(diào)用brk(sbrk)并傳入負數(shù)埂伦,即可縮小Heap區(qū)的大小思恐;如果是mmap分配的內(nèi)存沾谜,調(diào)用munmap歸還內(nèi)存膊毁。無論這兩種那種處理方式,都會立即縮減進程虛擬地址空間基跑,并歸還未使用的物理內(nèi)存給操作系統(tǒng)婚温。
brk(sbrk)和mmap都是系統(tǒng)調(diào)用,如果程序中頻繁的進行內(nèi)存的擴張和收縮媳否,每次都直接調(diào)用缭召,當然可以實現(xiàn)內(nèi)存精確管理的目的,但是隨之而來的性能損耗也很顯著逆日。目前大多數(shù)運行庫(glibc)等都對內(nèi)存管理做了一層封裝,避免每次直接調(diào)用系統(tǒng)調(diào)用影響性能萄凤。如此室抽,就涉及到運行庫的內(nèi)存分配的算法問題了。
在標準C庫中靡努,提供了malloc/free函數(shù)分配釋放內(nèi)存坪圾,這兩個函數(shù)底層是由brk,mmap惑朦,munmap這些系統(tǒng)調(diào)用實現(xiàn)的兽泄。
如何查看進程發(fā)生缺頁中斷的次數(shù)?
用ps -o majflt,minflt -C program命令查看漾月。
majflt代表major fault病梢,中文名叫大錯誤,minflt代表minor fault梁肿,中文名叫小錯誤蜓陌。這兩個數(shù)值表示一個進程自啟動以來所發(fā)生的缺頁中斷的次數(shù)。
發(fā)成缺頁中斷后吩蔑,執(zhí)行了那些操作钮热?
當一個進程發(fā)生缺頁中斷的時候,進程會陷入內(nèi)核態(tài)烛芬,執(zhí)行以下操作:
1隧期、檢查要訪問的虛擬地址是否合法
2、查找/分配一個物理頁
3赘娄、填充物理頁內(nèi)容(讀取磁盤仆潮,或者直接置0,或者啥也不干)
4擅憔、建立映射關(guān)系(虛擬地址到物理地址)
重新執(zhí)行發(fā)生缺頁中斷的那條指令
如果第3步鸵闪,需要讀取磁盤,那么這次缺頁中斷就是majflt暑诸,否則就是minflt蚌讼。
查看物理內(nèi)存頁使用情況:
cat /proc/$PID/smaps
辟灰,里面詳細記錄了該進程使用的物理頁內(nèi)存情況,如Private_Dirty篡石、Private_Clean等
mmap系統(tǒng)調(diào)用:讀寫MMAP映射區(qū)芥喇,相當于讀寫被映射的文件。本意是將文件當作內(nèi)存一樣讀寫凰萨。相比Read继控、Write,減少了內(nèi)存拷貝(Read胖眷、Write一個硬盤文件武通,需要先將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到應(yīng)用緩沖區(qū)(read),然后再將數(shù)據(jù)從應(yīng)用緩沖區(qū)拷貝回內(nèi)核緩沖區(qū)(write)珊搀。mmap直接將數(shù)據(jù)從內(nèi)核緩沖區(qū)映拷貝到另一個內(nèi)核緩沖區(qū))冶忱,但是被修改的數(shù)據(jù)從MMAP區(qū)同步到磁盤文件上,依賴于系統(tǒng)的頁管理算法境析,默認會慢條斯理得將內(nèi)容寫到磁盤上囚枪。另外提供了msync強制同步到磁盤上。
Glibc內(nèi)存分配算法
glibc的內(nèi)存分配算法劳淆,是基于dlmalloc實現(xiàn)的ptmalloc链沼,dlmalloc詳細可以參考A Memory Allocator或者我之前的文章Glibc內(nèi)存分配器。這里主要講下和內(nèi)存歸還策略相關(guān)的沛鸵,其他內(nèi)容不做過多擴展括勺。
整體來說,glibc采用的是dlmalloc曲掰。為了避免頻繁調(diào)用系統(tǒng)調(diào)用朝刊,它內(nèi)部維護了一個內(nèi)存池,方便reuse蜈缤,又稱為free-list或bins拾氓,如下圖示:
所有調(diào)用delete釋放的內(nèi)存,并不是立即調(diào)用brk(sbrk)歸還給操作系統(tǒng)底哥,而是先將這個內(nèi)存塊掛在free-list(bins)里面咙鞍,然后進行內(nèi)存歸并(可選操作,相鄰的可用內(nèi)存塊合并為更大的可用內(nèi)存塊)趾徽,并檢查是否達到malloc_trim的threshhold续滋,如果達到了,則調(diào)用malloc_trim歸還部分可用內(nèi)存給操作系統(tǒng)孵奶。
glibc中疲酌,設(shè)置了默認進行malloc_trim的threshhold為128K,也就是說當dlmalloc管理的內(nèi)存池中最大可用內(nèi)存>128K時,就會執(zhí)行malloc_trim操作朗恳,歸還部分內(nèi)存給操作系統(tǒng)湿颅;而在可用內(nèi)存<=128K時,及時程序中delete了這部分內(nèi)存粥诫,這些內(nèi)存也是不會歸還給操作系統(tǒng)的油航。表現(xiàn)為:調(diào)用delete之后,進程占用的內(nèi)存并沒有減少怀浆。
另外谊囚,部分glibc的默認設(shè)置如下:
DEFAULT_MXFAST 64 (for 32bit), 128 (for 64bit) // free-list(fastbin)最大內(nèi)存塊
DEFAULT_TRIM_THRESHOLD 128 * 1024 // malloc_trim的門檻值 128k
DEFAULT_TOP_PAD 0
DEFAULT_MMAP_THRESHOLD 128 * 1024 // 使用mmap分配內(nèi)存的門檻值 128k
DEFAULT_MMAP_MAX 65536 // mmap的最大數(shù)量
這些參數(shù)都可以通過mallopt進行調(diào)整。
malloc_trim(0)可以立即執(zhí)行trim操作执赡,將內(nèi)存還給操作系統(tǒng)镰踏。
具體fastbin相關(guān)的內(nèi)容,此處不做介紹沙合,前期有很多基于fastbin的堆溢出攻擊余境,感興趣的同學(xué)可以google關(guān)鍵字fastbin搜索下。
測試:
- 循環(huán)new分配64K * 2048的內(nèi)存空間灌诅,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放含末。top看進程依然使用131M內(nèi)存猜拾,沒有釋放。 ---- 此時用brk
- 循環(huán)new分配128K * 2048的內(nèi)存空間佣盒,寫入臟數(shù)據(jù)后挎袜,循環(huán)調(diào)用delete釋放。top看進程使用肥惭,2960字節(jié)內(nèi)存盯仪,完全釋放。 ---- 此時用mmap
- 設(shè)置M_MMAP_THRESHOLD 256k蜜葱,循環(huán)new分配128k * 2048 的內(nèi)存空間全景,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放牵囤,而后調(diào)用malloc_trim(0)爸黄。top看進程使用,2348字節(jié)揭鳞,完全釋放炕贵。 ----此時用brk
測試代碼如下:
int main(int argc, char* argv[])
{
mallopt(M_MMAP_THRESHOLD, 256*1024);
//mallopt(M_TRIM_THRESHOLD, 64*1024);
// MemoryLeak
int MEMORY_SIZE = hydra::CTrans::STOI(argv[1]);
vector<char*> Array;
for (int j=0; j<2064; j++)
{
char* Buff = new char[MEMORY_SIZE];
for (int i=0;i<MEMORY_SIZE;i++)
Buff[i] = i;
Array.push_back(Buff);
}
sleep(10);
for (int j=0; j<2064; j++)
delete []Array[j];
cout << "Delete All" << endl;
//sleep(10);
//malloc_trim(0);
//cout << "strim" << endl;
while(1) sleep(10);
}
一個例子來說明內(nèi)存分配的原理
情況一、malloc小于128k的內(nèi)存野崇,使用brk分配內(nèi)存称开,將_edata往高地址推(只分配虛擬空間,不對應(yīng)物理內(nèi)存(因此沒有初始化),第一次讀/寫數(shù)據(jù)時鳖轰,引起內(nèi)核缺頁中斷清酥,內(nèi)核才分配對應(yīng)的物理內(nèi)存,然后虛擬地址空間建立映射關(guān)系)脆霎,如下圖:
- 進程啟動的時候总处,其(虛擬)內(nèi)存空間的初始布局如圖1所示。
其中睛蛛,mmap內(nèi)存映射文件是在堆和棧的中間(例如libc-2.2.93.so鹦马,其它數(shù)據(jù)文件等),為了簡單起見忆肾,省略了內(nèi)存映射文件荸频。
_edata指針(glibc里面定義)指向數(shù)據(jù)段的最高地址。 - 進程調(diào)用A=malloc(30K)以后客冈,內(nèi)存空間如圖2:
malloc函數(shù)會調(diào)用brk系統(tǒng)調(diào)用旭从,將_edata指針往高地址推30K,就完成虛擬內(nèi)存分配场仲。
你可能會問:只要把_edata+30K就完成內(nèi)存分配了和悦?
事實是這樣的,_edata+30K只是完成虛擬地址的分配渠缕,A這塊內(nèi)存現(xiàn)在還是沒有物理頁與之對應(yīng)的鸽素,等到進程第一次讀寫A這塊內(nèi)存的時候,發(fā)生缺頁中斷亦鳞,這個時候馍忽,內(nèi)核才分配A這塊內(nèi)存對應(yīng)的物理頁。也就是說燕差,如果用malloc分配了A這塊內(nèi)容遭笋,然后從來不訪問它,那么徒探,A對應(yīng)的物理頁是不會被分配的瓦呼。 - 進程調(diào)用B=malloc(40K)以后,內(nèi)存空間如圖3测暗。
情況二吵血、malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存偷溺,在堆和棧之間找一塊空閑內(nèi)存分配(對應(yīng)獨立內(nèi)存蹋辅,而且初始化為0),如下圖:
- 進程調(diào)用C=malloc(200K)以后挫掏,內(nèi)存空間如圖4:
默認情況下侦另,malloc函數(shù)分配內(nèi)存,如果請求內(nèi)存大于128K(可由M_MMAP_THRESHOLD選項調(diào)節(jié)),那就不是去推_edata指針了褒傅,而是利用mmap系統(tǒng)調(diào)用弃锐,從堆和棧的中間分配一塊虛擬內(nèi)存。
這樣子做主要是因為::brk分配的內(nèi)存需要等到高地址內(nèi)存釋放以后才能釋放(例如殿托,在B釋放之前霹菊,A是不可能釋放的,這就是內(nèi)存碎片產(chǎn)生的原因支竹,什么時候緊縮看下面)旋廷,而mmap分配的內(nèi)存可以單獨釋放。
當然礼搁,還有其它的好處饶碘,也有壞處,再具體下去馒吴,有興趣的同學(xué)可以去看glibc里面malloc的代碼了扎运。 - 進程調(diào)用D=malloc(100K)以后,內(nèi)存空間如圖5饮戳;
-
進程調(diào)用free(C)以后豪治,C對應(yīng)的虛擬內(nèi)存和物理內(nèi)存一起釋放。
- 進程調(diào)用free(B)以后扯罐,如圖7所示:
B對應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒有釋放负拟,因為只有一個_edata指針,如果往回推篮赢,那么D這塊內(nèi)存怎么辦呢?當然琉挖,B這塊內(nèi)存启泣,是可以重用的,如果這個時候再來一個40K的請求示辈,那么malloc很可能就把B這塊內(nèi)存返回回去了寥茫。 - 進程調(diào)用free(D)以后,如圖8所示:
B和D連接起來矾麻,變成一塊140K的空閑內(nèi)存纱耻。 - 默認情況下:
當最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD選項調(diào)節(jié))時,執(zhí)行內(nèi)存緊縮操作(trim)险耀。在上一個步驟free的時候弄喘,發(fā)現(xiàn)最高地址空閑內(nèi)存超過128K,于是內(nèi)存緊縮甩牺,變成圖9所示城看。
結(jié)論
簡單來說耻台,文章開頭內(nèi)存不斷增長的趨勢的根本原因是:glibc
在利用操作系統(tǒng)的內(nèi)存構(gòu)建進程自身的內(nèi)存池强重。由于進程本身處理請求量大赫段,頻繁調(diào)用new
和delete
,在一段時間內(nèi)钉鸯,進程不斷的從操作系統(tǒng)獲取內(nèi)存來滿足新增的調(diào)用要求,但是從最終結(jié)果上來講,總有一個臨界點戒努,使得進程從操作系統(tǒng)新獲取的內(nèi)存和歸還給操作系統(tǒng)的內(nèi)存達成相對平衡。在這個動態(tài)平衡建立前镐躲,內(nèi)存會不斷增長储玫,直到到達臨界點。
按照這個理論匀油,機器內(nèi)存應(yīng)該先漲后平缘缚。我們看下幾天后,機器的內(nèi)存趨勢圖:
可以看出敌蚜,在系統(tǒng)內(nèi)存增長到3.7G左右時桥滨,整個機器的內(nèi)存處于動態(tài)平衡的階段,不再顯著增長弛车。由此驗證齐媒,我們的推斷是正確的。
經(jīng)驗
遇到如文章開頭所說的那種內(nèi)存不斷增長的情況纷跛,不要輕易斷定內(nèi)存泄漏喻括,先觀察一段時間再說。很可能是上文分析的原因贫奠。
參考文章
- A Memory Allocator(dlmalloc, glibc)
- Free/Delete Not Returning Memory To OS?
- Does calling free or delete ever release memory back to the “system”
- How is malloc() implemented internally? [duplicate]
- How do malloc() and free() work?
- 淺析Linux堆溢出之fastbin
- Unix環(huán)境高級編程
- 內(nèi)存分配的原理__進程分配內(nèi)存有兩種方式唬血,分別由兩個系統(tǒng)調(diào)用完成:brk和mmap(不考慮共享內(nèi)存)