一次"內(nèi)存泄漏"引發(fā)的血案

2017年末锻弓,手Q春節(jié)紅包項目期間辣辫,為保障活動期間服務(wù)正常穩(wěn)定,我對性能不佳的Ark Server進行了改造和重寫忧便。重編發(fā)布一段時間后族吻,結(jié)果發(fā)現(xiàn)新發(fā)布的Svr的機器內(nèi)存一直在上漲。如下圖示:

內(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)存分配操作newdelete上铛碑,那么只可能是這里有貓膩。
網(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ū)別大致如下:

  1. brk(sbrk)付呕,性能損耗少; mmap相對而言计福,性能損耗大
  2. 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拾氓,如下圖示:

free-list

所有調(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搜索下。

測試:

  1. 循環(huán)new分配64K * 2048的內(nèi)存空間灌诅,寫入臟數(shù)據(jù)后,循環(huán)調(diào)用delete釋放含末。top看進程依然使用131M內(nèi)存猜拾,沒有釋放。 ---- 此時用brk
  2. 循環(huán)new分配128K * 2048的內(nèi)存空間佣盒,寫入臟數(shù)據(jù)后挎袜,循環(huán)調(diào)用delete釋放。top看進程使用肥惭,2960字節(jié)內(nèi)存盯仪,完全釋放。 ---- 此時用mmap
  3. 設(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
64k Delete前內(nèi)存占用

64k Delete后內(nèi)存占用

128k Delete前內(nèi)存占用

128k Delete后內(nèi)存占用

測試代碼如下:

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)系)脆霎,如下圖:

  1. 進程啟動的時候总处,其(虛擬)內(nèi)存空間的初始布局如圖1所示。
    其中睛蛛,mmap內(nèi)存映射文件是在堆和棧的中間(例如libc-2.2.93.so鹦马,其它數(shù)據(jù)文件等),為了簡單起見忆肾,省略了內(nèi)存映射文件荸频。
    _edata指針(glibc里面定義)指向數(shù)據(jù)段的最高地址。
  2. 進程調(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)的物理頁是不會被分配的瓦呼。
  3. 進程調(diào)用B=malloc(40K)以后,內(nèi)存空間如圖3测暗。
    情況二吵血、malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存偷溺,在堆和棧之間找一塊空閑內(nèi)存分配(對應(yīng)獨立內(nèi)存蹋辅,而且初始化為0),如下圖:
  4. 進程調(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的代碼了扎运。
  5. 進程調(diào)用D=malloc(100K)以后,內(nèi)存空間如圖5饮戳;
  6. 進程調(diào)用free(C)以后豪治,C對應(yīng)的虛擬內(nèi)存和物理內(nèi)存一起釋放。


  7. 進程調(diào)用free(B)以后扯罐,如圖7所示:
    B對應(yīng)的虛擬內(nèi)存和物理內(nèi)存都沒有釋放负拟,因為只有一個_edata指針,如果往回推篮赢,那么D這塊內(nèi)存怎么辦呢?當然琉挖,B這塊內(nèi)存启泣,是可以重用的,如果這個時候再來一個40K的請求示辈,那么malloc很可能就把B這塊內(nèi)存返回回去了寥茫。
  8. 進程調(diào)用free(D)以后,如圖8所示:
    B和D連接起來矾麻,變成一塊140K的空閑內(nèi)存纱耻。
  9. 默認情況下:
    當最高地址空間的空閑內(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)用newdelete,在一段時間內(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)存趨勢圖:

內(nèi)存增長趨勢圖

可以看出敌蚜,在系統(tǒng)內(nèi)存增長到3.7G左右時桥滨,整個機器的內(nèi)存處于動態(tài)平衡的階段,不再顯著增長弛车。由此驗證齐媒,我們的推斷是正確的。

經(jīng)驗

遇到如文章開頭所說的那種內(nèi)存不斷增長的情況纷跛,不要輕易斷定內(nèi)存泄漏喻括,先觀察一段時間再說。很可能是上文分析的原因贫奠。


參考文章

  1. A Memory Allocator(dlmalloc, glibc)
  2. Free/Delete Not Returning Memory To OS?
  3. Does calling free or delete ever release memory back to the “system”
  4. How is malloc() implemented internally? [duplicate]
  5. How do malloc() and free() work?
  6. 淺析Linux堆溢出之fastbin
  7. Unix環(huán)境高級編程
  8. 內(nèi)存分配的原理__進程分配內(nèi)存有兩種方式唬血,分別由兩個系統(tǒng)調(diào)用完成:brk和mmap(不考慮共享內(nèi)存)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市唤崭,隨后出現(xiàn)的幾起案子拷恨,更是在濱河造成了極大的恐慌,老刑警劉巖谢肾,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腕侄,死亡現(xiàn)場離奇詭異,居然都是意外死亡芦疏,警方通過查閱死者的電腦和手機冕杠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酸茴,“玉大人分预,你說我怎么就攤上這事⌒胶矗” “怎么了噪舀?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵魁淳,是天一觀的道長。 經(jīng)常有香客問我与倡,道長界逛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任纺座,我火速辦了婚禮息拜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘净响。我一直安慰自己少欺,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布馋贤。 她就那樣靜靜地躺著赞别,像睡著了一般。 火紅的嫁衣襯著肌膚如雪配乓。 梳的紋絲不亂的頭發(fā)上仿滔,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音犹芹,去河邊找鬼崎页。 笑死,一個胖子當著我的面吹牛腰埂,可吹牛的內(nèi)容都是我干的飒焦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼屿笼,長吁一口氣:“原來是場噩夢啊……” “哼牺荠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起驴一,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤休雌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蛔趴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挑辆,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡例朱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年孝情,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洒嗤。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡箫荡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出渔隶,到底是詐尸還是另有隱情羔挡,我是刑警寧澤洁奈,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站绞灼,受9級特大地震影響利术,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜低矮,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一印叁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧军掂,春花似錦轮蜕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至终议,卻和暖如春汇竭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痊剖。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工韩玩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人陆馁。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓找颓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親叮贩。 傳聞我的和親對象是個殘疾皇子击狮,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容