2萬(wàn)字|帶您領(lǐng)略glibc內(nèi)存管理精髓
大家好,我是雨樂(lè)。
最近在逛知乎的時(shí)候,看到篇帖子,如下:
看了下面所有的回答,要么是沒(méi)有回答到點(diǎn)上,要么是回答不夠深入,所以,借助本文,深入講解C/C++內(nèi)存管理。
1 寫(xiě)在前面
源碼分析本身就很枯燥乏味,尤其是要將其寫(xiě)成通俗易懂的文章,更是難上加難。
本文盡可能的從讀者角度去進(jìn)行分析,重點(diǎn)寫(xiě)大家關(guān)心的點(diǎn),必要的時(shí)候,會(huì)貼出部分源碼,以加深大家的理解,盡可能的通過(guò)本文,讓大家理解內(nèi)存分配釋放的本質(zhì)原理。
接下來(lái)的內(nèi)容,干貨滿滿,對(duì)于你我都是一次收獲的過(guò)程。主要從內(nèi)存布局、glibc內(nèi)存管理、malloc實(shí)現(xiàn)以及free實(shí)現(xiàn)幾個(gè)點(diǎn)來(lái)帶你遨游glibc內(nèi)存管理的實(shí)質(zhì)。最后,針對(duì)項(xiàng)目中的問(wèn)題,指出了解決方案。大綱內(nèi)容如下:
2 背景
幾年前,在上家公司做了一個(gè)項(xiàng)目,暫且稱之為SeedService吧。SeedService從kafka獲取feed流的轉(zhuǎn)、評(píng)、贊信息,加載到內(nèi)存。因?yàn)槊刻鞌?shù)據(jù)不一樣,所以kafka的topic也是按天來(lái)切分,整個(gè)server內(nèi)部,類似于雙指針實(shí)現(xiàn),當(dāng)天0點(diǎn)釋放頭一天的數(shù)據(jù)。
項(xiàng)目上線,一切運(yùn)行正常。
但是幾天之后,進(jìn)程開(kāi)始無(wú)緣無(wú)故的消失。開(kāi)始定位問(wèn)題,最終發(fā)現(xiàn)是因?yàn)閮?nèi)存暴增導(dǎo)致OOM,最終被操作系統(tǒng)kill掉。
弄清楚了進(jìn)程消失的原因之后,開(kāi)始著手分析內(nèi)存泄漏。在解決了幾個(gè)內(nèi)存泄露的潛在問(wèn)題以后,發(fā)現(xiàn)系統(tǒng)在高壓力高并發(fā)環(huán)境下長(zhǎng)時(shí)間運(yùn)行仍然會(huì)發(fā)生內(nèi)存暴增的現(xiàn)象,最終進(jìn)程因OOM被操作系統(tǒng)殺掉。
由于內(nèi)存管理不外乎三個(gè)層面,用戶管理層,C 運(yùn)行時(shí)庫(kù)層,操作系統(tǒng)層,在操作系統(tǒng)層發(fā)現(xiàn)進(jìn)程的內(nèi)存暴增,同時(shí)又確認(rèn)了用戶管理層沒(méi)有內(nèi)存泄露,因此懷疑是 C 運(yùn)行時(shí)庫(kù)的問(wèn)題,也就是Glibc 的內(nèi)存管理方式導(dǎo)致了進(jìn)程的內(nèi)存暴增。
問(wèn)題縮小到glibc的內(nèi)存管理方面,把下面幾個(gè)問(wèn)題弄清楚,才能解決SeedService進(jìn)程消失的問(wèn)題:
glibc 在什么情況下不會(huì)將內(nèi)存歸還給操作系統(tǒng)?
glibc 的內(nèi)存管理方式有哪些約束?適合什么樣的內(nèi)存分配場(chǎng)景?
我們的系統(tǒng)中的內(nèi)存管理方式是與glibc 的內(nèi)存管理的約束相悖的?
glibc 是如何管理內(nèi)存的?
帶著上面這些問(wèn)題,大概用了將近一個(gè)月的時(shí)間分析了glibc運(yùn)行時(shí)庫(kù)的內(nèi)存管理代碼,今天將當(dāng)時(shí)的筆記整理了出來(lái),希望能夠?qū)Υ蠹矣杏谩?/p>
3 基礎(chǔ)
Linux 系統(tǒng)在裝載 elf 格式的程序文件時(shí),會(huì)調(diào)用 loader 把可執(zhí)行文件中的各個(gè)段依次載入到從某一地址開(kāi)始的空間中。
用戶程序可以直接使用系統(tǒng)調(diào)用來(lái)管理 heap 和mmap 映射區(qū)域,但更多的時(shí)候程序都是使用 C 語(yǔ)言提供的 malloc()和 free()函數(shù)來(lái)動(dòng)態(tài)的分配和釋放內(nèi)存。stack區(qū)域是唯一不需要映射,用戶卻可以訪問(wèn)的內(nèi)存區(qū)域,這也是利用堆棧溢出進(jìn)行攻擊的基礎(chǔ)。
3.1 進(jìn)程內(nèi)存布局
計(jì)算機(jī)系統(tǒng)分為32位和64位,而32位和64位的進(jìn)程布局是不一樣的,即使是同為32位系統(tǒng),其布局依賴于內(nèi)核版本,也是不同的。
在介紹詳細(xì)的內(nèi)存布局之前,我們先描述幾個(gè)概念:
棧區(qū)(Stack)— 存儲(chǔ)程序執(zhí)行期間的本地變量和函數(shù)的參數(shù),從高地址向低地址生長(zhǎng)
堆區(qū)(Heap)動(dòng)態(tài)內(nèi)存分配區(qū)域,通過(guò) malloc、new、free 和 delete 等函數(shù)管理
未初始化變量區(qū)(BSS)— 存儲(chǔ)未被初始化的全局變量和靜態(tài)變量
數(shù)據(jù)區(qū)(Data)— 存儲(chǔ)在源代碼中有預(yù)定義值的全局變量和靜態(tài)變量
代碼區(qū)(Text)— 存儲(chǔ)只讀的程序執(zhí)行代碼,即機(jī)器指令
在Linux內(nèi)核2.6.7以前,進(jìn)程內(nèi)存布局如下圖所示。
在該內(nèi)存布局示例圖中,mmap 區(qū)域與棧區(qū)域相對(duì)增長(zhǎng),這意味著堆只有 1GB 的虛擬地址空間可以使用,繼續(xù)增長(zhǎng)就會(huì)進(jìn)入 mmap 映射區(qū)域, 這顯然不是我們想要的。這是由于 32 模式地址空間限制造成的,所以內(nèi)核引入了另一種虛擬地址空間的布局形式。但對(duì)于 64 位系統(tǒng),因?yàn)樘峁┝司薮蟮奶摂M地址空間,所以64位系統(tǒng)就采用的這種布局方式。
如上所示,由于經(jīng)典內(nèi)存布局具有空間局限性,因此在內(nèi)核2.6.7以后,就引入了下圖這種默認(rèn)進(jìn)程布局方式。
從上圖可以看到,棧至頂向下擴(kuò)展,并且棧是有界的。堆至底向上擴(kuò)展,mmap 映射區(qū)域至頂向下擴(kuò)展,mmap 映射區(qū)域和堆相對(duì)擴(kuò)展,直至耗盡虛擬地址空間中的剩余區(qū)域,這種結(jié)構(gòu)便于C運(yùn)行時(shí)庫(kù)使用 mmap 映射區(qū)域和堆進(jìn)行內(nèi)存分配。
如之前所述,64位進(jìn)程內(nèi)存布局方式由于其地址空間足夠,且實(shí)現(xiàn)方便,所以采用的與32位經(jīng)典內(nèi)存布局的方式一致,如下圖所示:
3.2 操作系統(tǒng)內(nèi)存分配函數(shù)
在之前介紹內(nèi)存布局的時(shí)候,有提到過(guò),heap 和mmap 映射區(qū)域是可以提供給用戶程序使用的虛擬內(nèi)存空間。那么我們?cè)撊绾潍@得該區(qū)域的內(nèi)存呢?
操作系統(tǒng)提供了相關(guān)的系統(tǒng)調(diào)用來(lái)完成內(nèi)存分配工作。
對(duì)于heap的操作,操作系統(tǒng)提供了brk()函數(shù),c運(yùn)行時(shí)庫(kù)提供了sbrk()函數(shù)。
對(duì)于mmap映射區(qū)域的操作,操作系統(tǒng)提供了mmap()和munmap()函數(shù)。
sbrk(),brk() 或者 mmap() 都可以用來(lái)向我們的進(jìn)程添加額外的虛擬內(nèi)存。而glibc就是使用這些函數(shù)來(lái)向操作系統(tǒng)申請(qǐng)?zhí)摂M內(nèi)存,以完成內(nèi)存分配的。
這里要提到一個(gè)很重要的概念,內(nèi)存的延遲分配,只有在真正訪問(wèn)一個(gè)地址的時(shí)候才建立這個(gè)地址的物理映射,這是 Linux 內(nèi)存管理的基本思想之一。Linux 內(nèi)核在用戶申請(qǐng)內(nèi)存的時(shí)候,只是給它分配了一個(gè)線性區(qū)(也就是虛擬內(nèi)存),并沒(méi)有分配實(shí)際物理內(nèi)存;只有當(dāng)用戶使用這塊內(nèi)存的時(shí)候,內(nèi)核才會(huì)分配具體的物理頁(yè)面給用戶,這時(shí)候才占用寶貴的物理內(nèi)存。內(nèi)核釋放物理頁(yè)面是通過(guò)釋放線性區(qū),找到其所對(duì)應(yīng)的物理頁(yè)面,將其全部釋放的過(guò)程。
進(jìn)程的內(nèi)存結(jié)構(gòu),在內(nèi)核中,是用mm_struct來(lái)表示的,其定義如下:
struct mm_struct { ... unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, unsigned long pgoff, unsigned long flags); ... unsigned long mmap_base; /* base of mmap area */ unsigned long task_size; /* size of task vm space */ ... unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; ... }
在上述mm_struct結(jié)構(gòu)中:
[start_code,end_code)表示代碼段的地址空間范圍。
[start_data,end_start)表示數(shù)據(jù)段的地址空間范圍。
[start_brk,brk)分別表示heap段的起始空間和當(dāng)前的heap指針。
[start_stack,end_stack)表示stack段的地址空間范圍。
mmap_base表示memory mapping段的起始地址。
C語(yǔ)言的動(dòng)態(tài)內(nèi)存分配基本函數(shù)是 malloc(),在 Linux 上的實(shí)現(xiàn)是通過(guò)內(nèi)核的 brk 系統(tǒng)調(diào)用。brk()是一個(gè)非常簡(jiǎn)單的系統(tǒng)調(diào)用, 只是簡(jiǎn)單地改變mm_struct結(jié)構(gòu)的成員變量 brk 的值。
在前面有提過(guò),有兩個(gè)函數(shù)可以直接從堆(Heap)申請(qǐng)內(nèi)存,brk()函數(shù)為系統(tǒng)調(diào)用,sbrk()為c庫(kù)函數(shù)。
系統(tǒng)調(diào)用通常提過(guò)一種最小的功能,而庫(kù)函數(shù)相比系統(tǒng)調(diào)用,則提供了更復(fù)雜的功能。在glibc中,malloc就是調(diào)用sbrk()函數(shù)將數(shù)據(jù)段的下界移動(dòng)以來(lái)代表內(nèi)存的分配和釋放。sbrk()函數(shù)在內(nèi)核的管理下,將虛擬地址空間映射到內(nèi)存,供malloc()函數(shù)使用。
下面為brk()函數(shù)和sbrk()函數(shù)的聲明。
#include
需要說(shuō)明的是,當(dāng)sbrk()的參數(shù)increment為0時(shí)候,sbrk()返回的是進(jìn)程當(dāng)前brk值。increment 為正數(shù)時(shí)擴(kuò)展 brk 值,當(dāng) increment 為負(fù)值時(shí)收縮 brk 值。
在LINUX中我們可以使用mmap用來(lái)在進(jìn)程虛擬內(nèi)存地址空間中分配地址空間,創(chuàng)建和物理內(nèi)存的映射關(guān)系。
mmap()函數(shù)將一個(gè)文件或者其它對(duì)象映射進(jìn)內(nèi)存。文件被映射到多個(gè)頁(yè)上,如果文件的大小不是所有頁(yè)的大小之和,最后一個(gè)頁(yè)不被使用的空間將會(huì)清零。
munmap 執(zhí)行相反的操作,刪除特定地址區(qū)域的對(duì)象映射。
函數(shù)的定義如下:
#include
·
映射關(guān)系分為以下兩種:
文件映射: 磁盤(pán)文件映射進(jìn)程的虛擬地址空間,使用文件內(nèi)容初始化物理內(nèi)存。
匿名映射: 初始化全為0的內(nèi)存空間
映射關(guān)系是否共享,可以分為:
私有映射(MAP_PRIVATE)
多進(jìn)程間數(shù)據(jù)共享,修改不反應(yīng)到磁盤(pán)實(shí)際文件,是一個(gè)copy-on-write(寫(xiě)時(shí)復(fù)制)的映射方式。
共享映射(MAP_SHARED)
多進(jìn)程間數(shù)據(jù)共享,修改反應(yīng)到磁盤(pán)實(shí)際文件中。
因此,整個(gè)映射關(guān)系總結(jié)起來(lái)分為以下四種:
私有文件映射
多個(gè)進(jìn)程使用同樣的物理內(nèi)存頁(yè)進(jìn)行初始化,但是各個(gè)進(jìn)程對(duì)內(nèi)存文件的修改不會(huì)共享,也不會(huì)反應(yīng)到物理文件中
私有匿名映射
mmap會(huì)創(chuàng)建一個(gè)新的映射,各個(gè)進(jìn)程不共享,這種使用主要用于分配內(nèi)存(malloc分配大內(nèi)存會(huì)調(diào)用mmap)。例如開(kāi)辟新進(jìn)程時(shí),會(huì)為每個(gè)進(jìn)程分配虛擬的地址空間,這些虛擬地址映射的物理內(nèi)存空間各個(gè)進(jìn)程間讀的時(shí)候共享,寫(xiě)的時(shí)候會(huì)copy-on-write。
共享文件映射
多個(gè)進(jìn)程通過(guò)虛擬內(nèi)存技術(shù)共享同樣的物理內(nèi)存空間,對(duì)內(nèi)存文件的修改會(huì)反應(yīng)到實(shí)際物理文件中,也是進(jìn)程間通信(IPC)的一種機(jī)制。
共享匿名映射
這種機(jī)制在進(jìn)行fork的時(shí)候不會(huì)采用寫(xiě)時(shí)復(fù)制,父子進(jìn)程完全共享同樣的物理內(nèi)存頁(yè),這也就實(shí)現(xiàn)了父子進(jìn)程通信(IPC)。
這里值得注意的是,mmap只是在虛擬內(nèi)存分配了地址空間,只有在第一次訪問(wèn)虛擬內(nèi)存的時(shí)候才分配物理內(nèi)存。
在mmap之后,并沒(méi)有在將文件內(nèi)容加載到物理頁(yè)上,只有在虛擬內(nèi)存中分配了地址空間。當(dāng)進(jìn)程在訪問(wèn)這段地址時(shí),通過(guò)查找頁(yè)表,發(fā)現(xiàn)虛擬內(nèi)存對(duì)應(yīng)的頁(yè)沒(méi)有在物理內(nèi)存中緩存,則產(chǎn)生"缺頁(yè)",由內(nèi)核的缺頁(yè)異常處理程序處理,將文件對(duì)應(yīng)內(nèi)容,以頁(yè)為單位(4096)加載到物理內(nèi)存,注意是只加載缺頁(yè),但也會(huì)受操作系統(tǒng)一些調(diào)度策略影響,加載的比所需的多。
下面的內(nèi)容將是本文的重點(diǎn)中的重點(diǎn),對(duì)于了解內(nèi)存布局以及后面glibc的內(nèi)存分配原理至關(guān)重要,必要的話,可以多閱讀幾次。
4 概述
在前面,我們有提到在堆上分配內(nèi)存有兩個(gè)函數(shù),分別為brk()系統(tǒng)調(diào)用和sbrk()c運(yùn)行時(shí)庫(kù)函數(shù),在內(nèi)存映射區(qū)分配內(nèi)存有mmap函數(shù)。
現(xiàn)在我們假設(shè)一種情況,如果每次分配,都直接使用brk(),sbrk()或者mmap()函數(shù)進(jìn)行多次內(nèi)存分配。如果程序頻繁的進(jìn)行內(nèi)存分配和釋放,都是和操作系統(tǒng)直接打交道,那么性能可想而知。
這就引入了一個(gè)概念,內(nèi)存管理。
本節(jié)大綱如下:
4.1 內(nèi)存管理
內(nèi)存管理是指軟件運(yùn)行時(shí)對(duì)計(jì)算機(jī)內(nèi)存資源的分配和使用的技術(shù)。其最主要的目的是如何高效,快速的分配,并且在適當(dāng)?shù)臅r(shí)候釋放和回收內(nèi)存資源。
一個(gè)好的內(nèi)存管理器,需要具有以下特點(diǎn):
1、跨平臺(tái)、可移植
通常情況下,內(nèi)存管理器向操作系統(tǒng)申請(qǐng)內(nèi)存,然后進(jìn)行再次分配。所以,針對(duì)不同的操作系統(tǒng),內(nèi)存管理器就需要支持操作系統(tǒng)兼容,讓使用者在跨平臺(tái)的操作上沒(méi)有區(qū)別。
2、浪費(fèi)空間小
內(nèi)存管理器管理內(nèi)存,如果內(nèi)存浪費(fèi)比較大,那么顯然這就不是一個(gè)優(yōu)秀的內(nèi)存管理器。
通常說(shuō)的內(nèi)存碎片,就是浪費(fèi)空間的罪魁禍?zhǔn)祝魞?nèi)存管理器中有大量的內(nèi)存碎片,它們是一些不連續(xù)的小塊內(nèi)存,它們總量可能很大,但無(wú)法使用,這顯然也不是一個(gè)優(yōu)秀的內(nèi)存管理器。
3、速度快
之所以使用內(nèi)存管理器,根本原因就是為了分配/釋放快。
4、調(diào)試功能
作為一個(gè) C/C++程序員,內(nèi)存錯(cuò)誤可以說(shuō)是我們的噩夢(mèng),上一次的內(nèi)存錯(cuò)誤一定還讓你記憶猶新。內(nèi)存管理器提供的調(diào)試功能,強(qiáng)大易用,特別對(duì)于嵌入式環(huán)境來(lái)說(shuō),內(nèi)存錯(cuò)誤檢測(cè)工具缺乏,內(nèi)存管理器提供的調(diào)試功能就更是不可或缺了。
4.2 管理方式
內(nèi)存管理的管理方式,分為 手動(dòng)管理 和 自動(dòng)管理 兩種。
所謂的手動(dòng)管理,就是使用者在申請(qǐng)內(nèi)存的時(shí)候使用malloc等函數(shù)進(jìn)行申請(qǐng),在需要釋放的時(shí)候,需要調(diào)用free函數(shù)進(jìn)行釋放。一旦用過(guò)的內(nèi)存沒(méi)有釋放,就會(huì)造成內(nèi)存泄漏,占用更多的系統(tǒng)內(nèi)存;如果在使用結(jié)束前釋放,會(huì)導(dǎo)致危險(xiǎn)的懸掛指針,其他對(duì)象指向的內(nèi)存已經(jīng)被系統(tǒng)回收或者重新使用。
自動(dòng)管理內(nèi)存由編程語(yǔ)言的內(nèi)存管理系統(tǒng)自動(dòng)管理,在大多數(shù)情況下不需要使用者的參與,能夠自動(dòng)釋放不再使用的內(nèi)存。
手動(dòng)管理內(nèi)存是一種比較傳統(tǒng)的內(nèi)存管理方式,C/C++ 這類系統(tǒng)級(jí)的編程語(yǔ)言不包含狹義上的自動(dòng)內(nèi)存管理機(jī)制,使用者需要主動(dòng)申請(qǐng)或者釋放內(nèi)存。
經(jīng)驗(yàn)豐富的工程師能夠精準(zhǔn)的確定內(nèi)存的分配和釋放時(shí)機(jī),人肉的內(nèi)存管理策略只要做到足夠精準(zhǔn),使用手動(dòng)管理內(nèi)存的方式可以提高程序的運(yùn)行性能,也不會(huì)造成內(nèi)存安全問(wèn)題。
但是,畢竟這種經(jīng)驗(yàn)豐富且能精準(zhǔn)確定內(nèi)存和分配釋放實(shí)際的使用者還是比較少的,只要是人工處理,總會(huì)帶來(lái)一些錯(cuò)誤,內(nèi)存泄漏和懸掛指針基本是 C/C++ 這類語(yǔ)言中最常出現(xiàn)的錯(cuò)誤,手動(dòng)的內(nèi)存管理也會(huì)占用工程師的大量精力,很多時(shí)候都需要思考對(duì)象應(yīng)該分配到棧上還是堆上以及堆上的內(nèi)存應(yīng)該何時(shí)釋放,維護(hù)成本相對(duì)來(lái)說(shuō)還是比較高的,這也是必然要做的權(quán)衡。
自動(dòng)管理內(nèi)存基本是現(xiàn)代編程語(yǔ)言的標(biāo)配,因?yàn)閮?nèi)存管理模塊的功能非常確定,所以我們可以在編程語(yǔ)言的編譯期或者運(yùn)行時(shí)中引入自動(dòng)的內(nèi)存管理方式,最常見(jiàn)的自動(dòng)內(nèi)存管理機(jī)制就是垃圾回收,不過(guò)除了垃圾回收之外,一些編程語(yǔ)言也會(huì)使用自動(dòng)引用計(jì)數(shù)輔助內(nèi)存的管理。
自動(dòng)的內(nèi)存管理機(jī)制可以幫助工程師節(jié)省大量的與內(nèi)存打交道的時(shí)間,讓使用者將全部的精力都放在核心的業(yè)務(wù)邏輯上,提高開(kāi)發(fā)的效率;在一般情況下,這種自動(dòng)的內(nèi)存管理機(jī)制都可以很好地解決內(nèi)存泄漏和懸掛指針的問(wèn)題,但是這也會(huì)帶來(lái)額外開(kāi)銷并影響語(yǔ)言的運(yùn)行時(shí)性能。
4.1 常見(jiàn)的內(nèi)存管理器
1、ptmalloc
ptmalloc是隸屬于glibc(GNU Libc)的一款內(nèi)存分配器,現(xiàn)在在Linux環(huán)境上,我們使用的運(yùn)行庫(kù)的內(nèi)存分配(malloc/new)和釋放(free/delete)就是由其提供。
2、 BSD Malloc:BSD Malloc 是隨 4.2 BSD 發(fā)行的實(shí)現(xiàn),包含在 FreeBSD 之中,這個(gè)分配程序可以從預(yù)先確實(shí)大小的對(duì)象構(gòu)成的池中分配對(duì)象。它有一些用于對(duì)象大小的size 類,這些對(duì)象的大小為 2 的若干次冪減去某一常數(shù)。所以,如果您請(qǐng)求給定大小的一個(gè)對(duì)象,它就簡(jiǎn)單地分配一個(gè)與之匹配的 size 類。這樣就提供了一個(gè)快速的實(shí)現(xiàn),但是可能會(huì)浪費(fèi)內(nèi)存。
3、Hoard:編寫(xiě) Hoard 的目標(biāo)是使內(nèi)存分配在多線程環(huán)境中進(jìn)行得非常快。因此,它的構(gòu)造以鎖的使用為中心,從而使所有進(jìn)程不必等待分配內(nèi)存。它可以顯著地加快那些進(jìn)行很多分配和回收的多線程進(jìn)程的速度。
4、TCMalloc:Google 開(kāi)發(fā)的內(nèi)存分配器,在不少項(xiàng)目中都有使用,例如在 Golang 中就使用了類似的算法進(jìn)行內(nèi)存分配。它具有現(xiàn)代化內(nèi)存分配器的基本特征:對(duì)抗內(nèi)存碎片、在多核處理器能夠 scale。據(jù)稱,它的內(nèi)存分配速度是 glibc2.3 中實(shí)現(xiàn)的 malloc的數(shù)倍。
5 glibc之內(nèi)存管理(ptmalloc)
因?yàn)楸敬问鹿示褪怯玫倪\(yùn)行庫(kù)函數(shù)new/delete進(jìn)行的內(nèi)存分配和釋放,所以本文將著重分析glibc下的內(nèi)存分配庫(kù)ptmalloc。
本節(jié)大綱如下:
在c/c++中,我們分配內(nèi)存是在堆上進(jìn)行分配,那么這個(gè)堆,在glibc中是怎么表示的呢?
我們先看下堆的結(jié)構(gòu)聲明:
typedef struct _heap_info { mstate ar_ptr; /* Arena for this heap. */ struct _heap_info *prev; /* Previous heap. */ size_t size; /* Current size in bytes. */ size_t mprotect_size; /* Size in bytes that has been mprotected PROT_READ|PROT_WRITE. */ /* Make sure the following data is properly aligned, particularly that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of MALLOC_ALIGNMENT. */ char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
在堆的上述定義中,ar_ptr是指向分配區(qū)的指針,堆之間是以鏈表方式進(jìn)行連接,后面我會(huì)詳細(xì)講述進(jìn)程布局下,堆的結(jié)構(gòu)表示圖。
在開(kāi)始這部分之前,我們先了解下一些概念。
5.1 分配區(qū)(arena)
ptmalloc對(duì)進(jìn)程內(nèi)存是通過(guò)一個(gè)個(gè)Arena來(lái)進(jìn)行管理的。
在ptmalloc中,分配區(qū)分為主分配區(qū)(arena)和非主分配區(qū)(narena),分配區(qū)用struct malloc_state來(lái)表示。主分配區(qū)和非主分配區(qū)的區(qū)別是 主分配區(qū)可以使用sbrk和mmap向os申請(qǐng)內(nèi)存,而非分配區(qū)只能通過(guò)mmap向os申請(qǐng)內(nèi)存。
當(dāng)一個(gè)線程調(diào)用malloc申請(qǐng)內(nèi)存時(shí),該線程先查看線程私有變量中是否已經(jīng)存在一個(gè)分配區(qū)。如果存在,則對(duì)該分配區(qū)加鎖,加鎖成功的話就用該分配區(qū)進(jìn)行內(nèi)存分配;失敗的話則搜索環(huán)形鏈表找一個(gè)未加鎖的分配區(qū)。如果所有分配區(qū)都已經(jīng)加鎖,那么malloc會(huì)開(kāi)辟一個(gè)新的分配區(qū)加入環(huán)形鏈表并加鎖,用它來(lái)分配內(nèi)存。釋放操作同樣需要獲得鎖才能進(jìn)行。
需要注意的是,非主分配區(qū)是通過(guò)mmap向os申請(qǐng)內(nèi)存,一次申請(qǐng)64MB,一旦申請(qǐng)了,該分配區(qū)就不會(huì)被釋放,為了避免資源浪費(fèi),ptmalloc對(duì)分配區(qū)是有個(gè)數(shù)限制的。
對(duì)于32位系統(tǒng),分配區(qū)最大個(gè)數(shù) = 2 * CPU核數(shù) + 1
對(duì)于64位系統(tǒng),分配區(qū)最大個(gè)數(shù) = 8 * CPU核數(shù) + 1
堆管理結(jié)構(gòu):
struct malloc_state { mutex_t mutex; /* Serialize access. */ int flags; /* Flags (formerly in max_fast). */ #if THREAD_STATS /* Statistics for locking. Only used if THREAD_STATS is defined. */ long stat_lock_direct, stat_lock_loop, stat_lock_wait; #endif mfastbinptr fastbins[NFASTBINS]; /* Fastbins */ mchunkptr top; mchunkptr last_remainder; mchunkptr bins[NBINS * 2]; unsigned int binmap[BINMAPSIZE]; /* Bitmap of bins */ struct malloc_state *next; /* Linked list */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; };
每一個(gè)進(jìn)程只有一個(gè)主分配區(qū)和若干個(gè)非主分配區(qū)。主分配區(qū)由main線程或者第一個(gè)線程來(lái)創(chuàng)建持有。主分配區(qū)和非主分配區(qū)用環(huán)形鏈表連接起來(lái)。分配區(qū)內(nèi)有一個(gè)變量mutex以支持多線程訪問(wèn)。
在前面有提到,在每個(gè)分配區(qū)中都有一個(gè)變量mutex來(lái)支持多線程訪問(wèn)。每個(gè)線程一定對(duì)應(yīng)一個(gè)分配區(qū),但是一個(gè)分配區(qū)可以給多個(gè)線程使用,同時(shí)一個(gè)分配區(qū)可以由一個(gè)或者多個(gè)的堆組成,同一個(gè)分配區(qū)下的堆以鏈表方式進(jìn)行連接,它們之間的關(guān)系如下圖:
一個(gè)進(jìn)程的動(dòng)態(tài)內(nèi)存,由分配區(qū)管理,一個(gè)進(jìn)程內(nèi)有多個(gè)分配區(qū),一個(gè)分配區(qū)有多個(gè)堆,這就組成了復(fù)雜的進(jìn)程內(nèi)存管理結(jié)構(gòu)。
需要注意幾個(gè)點(diǎn):
主分配區(qū)通過(guò)brk進(jìn)行分配,非主分配區(qū)通過(guò)mmap進(jìn)行分配
非主分配區(qū)雖然是mmap分配,但是和大于128K直接使用mmap分配沒(méi)有任何聯(lián)系。大于128K的內(nèi)存使用mmap分配,使用完之后直接用ummap還給系統(tǒng)
每個(gè)線程在malloc會(huì)先獲取一個(gè)area,使用area內(nèi)存池分配自己的內(nèi)存,這里存在競(jìng)爭(zhēng)問(wèn)題
為了避免競(jìng)爭(zhēng),我們可以使用線程局部存儲(chǔ),thread cache(tcmalloc中的tc正是此意),線程局部存儲(chǔ)對(duì)area的改進(jìn)原理如下:
如果需要在一個(gè)線程內(nèi)部的各個(gè)函數(shù)調(diào)用都能訪問(wèn)、但其它線程不能訪問(wèn)的變量(被稱為static memory local to a thread 線程局部靜態(tài)變量),就需要新的機(jī)制來(lái)實(shí)現(xiàn)。這就是TLS。
thread cache本質(zhì)上是在static區(qū)為每一個(gè)thread開(kāi)辟一個(gè)獨(dú)有的空間,因?yàn)楠?dú)有,不再有競(jìng)爭(zhēng)
每次malloc時(shí),先去線程局部存儲(chǔ)空間中找area,用thread cache中的area分配存在thread area中的chunk。當(dāng)不夠時(shí)才去找堆區(qū)的area
5.2 chunk
ptmalloc通過(guò)malloc_chunk來(lái)管理內(nèi)存,給User data前存儲(chǔ)了一些信息,使用邊界標(biāo)記區(qū)分各個(gè)chunk。
chunk定義如下:
struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
prev_size: 如果前一個(gè)chunk是空閑的,則該域表示前一個(gè)chunk的大小,如果前一個(gè)chunk不空閑,該域無(wú)意義。
一段連續(xù)的內(nèi)存被分成多個(gè)chunk,prev_size記錄的就是相鄰的前一個(gè)chunk的size,知道當(dāng)前chunk的地址,減去prev_size便是前一個(gè)chunk的地址。prev_size主要用于相鄰空閑chunk的合并。
size :當(dāng)前 chunk 的大小,并且記錄了當(dāng)前 chunk 和前一個(gè) chunk 的一些屬性,包括前一個(gè) chunk 是否在使用中,當(dāng)前 chunk 是否是通過(guò) mmap 獲得的內(nèi)存,當(dāng)前 chunk 是否屬于非主分配區(qū)。
fd 和 bk : 指針 fd 和 bk 只有當(dāng)該 chunk 塊空閑時(shí)才存在,其作用是用于將對(duì)應(yīng)的空閑 chunk 塊加入到空閑chunk 塊鏈表中統(tǒng)一管理,如果該 chunk 塊被分配給應(yīng)用程序使用,那么這兩個(gè)指針也就沒(méi)有用(該 chunk 塊已經(jīng)從空閑鏈中拆出)了,所以也當(dāng)作應(yīng)用程序的使用空間,而不至于浪費(fèi)。
fd_nextsize 和 bk_nextsize: 當(dāng)前的 chunk 存在于 large bins 中時(shí), large bins 中的空閑 chunk 是按照大小排序的,但同一個(gè)大小的 chunk 可能有多個(gè),增加了這兩個(gè)字段可以加快遍歷空閑 chunk ,并查找滿足需要的空閑 chunk , fd_nextsize 指向下一個(gè)比當(dāng)前 chunk 大小大的第一個(gè)空閑 chunk , bk_nextszie 指向前一個(gè)比當(dāng)前 chunk 大小小的第一個(gè)空閑 chunk 。(同一大小的chunk可能有多塊,在總體大小有序的情況下,要想找到下一個(gè)比自己大或小的chunk,需要遍歷所有相同的chunk,所以才有fd_nextsize和bk_nextsize這種設(shè)計(jì)) 如果該 chunk 塊被分配給應(yīng)用程序使用,那么這兩個(gè)指針也就沒(méi)有用(該chunk 塊已經(jīng)從 size 鏈中拆出)了,所以也當(dāng)作應(yīng)用程序的使用空間,而不至于浪費(fèi)。
正如上面所描述,在ptmalloc中,為了盡可能的節(jié)省內(nèi)存,使用中的chunk和未使用的chunk在結(jié)構(gòu)上是不一樣的。
在上圖中:
chunk指針指向chunk開(kāi)始的地址
mem指針指向用戶內(nèi)存塊開(kāi)始的地址。
p=0時(shí),表示前一個(gè)chunk為空閑,prev_size才有效
p=1時(shí),表示前一個(gè)chunk正在使用,prev_size無(wú)效 p主要用于內(nèi)存塊的合并操作;ptmalloc 分配的第一個(gè)塊總是將p設(shè)為1, 以防止程序引用到不存在的區(qū)域
M=1 為mmap映射區(qū)域分配;M=0為heap區(qū)域分配
A=0 為主分配區(qū)分配;A=1 為非主分配區(qū)分配。
與非空閑chunk相比,空閑chunk在用戶區(qū)域多了四個(gè)指針,分別為fd,bk,fd_nextsize,bk_nextsize,這幾個(gè)指針的含義在上面已經(jīng)有解釋,在此不再贅述。
5.3 空閑鏈表(bins)
用戶調(diào)用free函數(shù)釋放內(nèi)存的時(shí)候,ptmalloc并不會(huì)立即將其歸還操作系統(tǒng),而是將其放入空閑鏈表(bins)中,這樣下次再調(diào)用malloc函數(shù)申請(qǐng)內(nèi)存的時(shí)候,就會(huì)從bins中取出一塊返回,這樣就避免了頻繁調(diào)用系統(tǒng)調(diào)用函數(shù),從而降低內(nèi)存分配的開(kāi)銷。
在ptmalloc中,會(huì)將大小相似的chunk鏈接起來(lái),叫做bin。總共有128個(gè)bin供ptmalloc使用。
根據(jù)chunk的大小,ptmalloc將bin分為以下幾種:
fast bin
unsorted bin
small bin
large bin
從前面malloc_state結(jié)構(gòu)定義,對(duì)bin進(jìn)行分類,可以分為fast bin和bins,其中unsorted bin、small bin 以及 large bin屬于bins。
在glibc中,上述4中bin的個(gè)數(shù)都不等,如下圖所示:
程序在運(yùn)行時(shí)會(huì)經(jīng)常需要申請(qǐng)和釋放一些較小的內(nèi)存空間。當(dāng)分配器合并了相鄰的幾個(gè)小的 chunk 之后,也許馬上就會(huì)有另一個(gè)小塊內(nèi)存的請(qǐng)求,這樣分配器又需要從大的空閑內(nèi)存中切分出一塊,這樣無(wú)疑是比較低效的,故而,malloc 中在分配過(guò)程中引入了 fast bins。
在前面malloc_state定義中
mfastbinptr fastbins[NFASTBINS]; // NFASTBINS = 10
fast bin的個(gè)數(shù)是10個(gè)
每個(gè)fast bin都是一個(gè)單鏈表(只使用fd指針)。這是因?yàn)閒ast bin無(wú)論是添加還是移除chunk都是在鏈表尾進(jìn)行操作,也就是說(shuō),對(duì)fast bin中chunk的操作,采用的是LIFO(后入先出)算法:添加操作(free內(nèi)存)就是將新的fast chunk加入鏈表尾,刪除操作(malloc內(nèi)存)就是將鏈表尾部的fast chunk刪除。
chunk size:10個(gè)fast bin中所包含的chunk size以8個(gè)字節(jié)逐漸遞增,即第一個(gè)fast bin中chunk size均為16個(gè)字節(jié),第二個(gè)fast bin的chunk size為24字節(jié),以此類推,最后一個(gè)fast bin的chunk size為80字節(jié)。
不會(huì)對(duì)free chunk進(jìn)行合并操作。這是因?yàn)閒ast bin設(shè)計(jì)的初衷就是小內(nèi)存的快速分配和釋放,因此系統(tǒng)將屬于fast bin的chunk的P(未使用標(biāo)志位)總是設(shè)置為1,這樣即使當(dāng)fast bin中有某個(gè)chunk同一個(gè)free chunk相鄰的時(shí)候,系統(tǒng)也不會(huì)進(jìn)行自動(dòng)合并操作,而是保留兩者。
malloc操作:在malloc的時(shí)候,如果申請(qǐng)的內(nèi)存大小范圍在fast bin的范圍內(nèi),則先在fast bin中查找,如果找到了,則返回。否則則從small bin、unsorted bin以及l(fā)arge bin中查找。
free操作:先通過(guò)chunksize函數(shù)根據(jù)傳入的地址指針獲取該指針對(duì)應(yīng)的chunk的大小;然后根據(jù)這個(gè)chunk大小獲取該chunk所屬的fast bin,然后再將此chunk添加到該fast bin的鏈尾即可。
下面是fastbin結(jié)構(gòu)圖:
unsorted bin 的隊(duì)列使用 bins 數(shù)組的第一個(gè),是bins的一個(gè)緩沖區(qū),加快分配的速度。當(dāng)用戶釋放的內(nèi)存大于max_fast或者fast bins合并后的chunk都會(huì)首先進(jìn)入unsorted bin上。
在unsorted bin中,chunk的size 沒(méi)有限制,也就是說(shuō)任何大小chunk都可以放進(jìn)unsorted bin中。這主要是為了讓“glibc malloc機(jī)制”能夠有第二次機(jī)會(huì)重新利用最近釋放的chunk(第一次機(jī)會(huì)就是fast bin機(jī)制)。利用unsorted bin,可以加快內(nèi)存的分配和釋放操作,因?yàn)檎麄€(gè)操作都不再需要花費(fèi)額外的時(shí)間去查找合適的bin了。
用戶malloc時(shí),如果在 fast bins 中沒(méi)有找到合適的 chunk,則malloc 會(huì)先在 unsorted bin 中查找合適的空閑 chunk,如果沒(méi)有合適的bin,ptmalloc會(huì)將unsorted bin上的chunk放入bins上,然后到bins上查找合適的空閑chunk。
與fast bin所不同的是,unsortedbin采用的遍歷順序是FIFO。
unsorted bin結(jié)構(gòu)圖如下:
大小小于512字節(jié)的chunk被稱為small chunk,而保存small chunks的bin被稱為small bin。數(shù)組從2開(kāi)始編號(hào),前62個(gè)bin為small bins,small bin每個(gè)bin之間相差8個(gè)字節(jié),同一個(gè)small bin中的chunk具有相同大小。
每個(gè)small bin都包括一個(gè)空閑區(qū)塊的雙向循環(huán)鏈表(也稱binlist)。free掉的chunk添加在鏈表的前端,而所需chunk則從鏈表后端摘除。
兩個(gè)毗連的空閑chunk會(huì)被合并成一個(gè)空閑chunk。合并消除了碎片化的影響但是減慢了free的速度。
分配時(shí),當(dāng)samll bin非空后,相應(yīng)的bin會(huì)摘除binlist中最后一個(gè)chunk并返回給用戶。
在free一個(gè)chunk的時(shí)候,檢查其前或其后的chunk是否空閑,若是則合并,也即把它們從所屬的鏈表中摘除并合并成一個(gè)新的chunk,新chunk會(huì)添加在unsorted bin鏈表的前端。
small bin也采用的是FIFO算法,即內(nèi)存釋放操作就將新釋放的chunk添加到鏈表的front end(前端),分配操作就從鏈表的rear end(尾端)中獲取chunk。
大小大于等于512字節(jié)的chunk被稱為large chunk,而保存large chunks的bin被稱為large bin,位于small bins后面。large bins中的每一個(gè)bin分別包含了一個(gè)給定范圍內(nèi)的chunk,其中的chunk按大小遞減排序,大小相同則按照最近使用時(shí)間排列。
兩個(gè)毗連的空閑chunk會(huì)被合并成一個(gè)空閑chunk。
small bins 的策略非常適合小分配,但我們不能為每個(gè)可能的塊大小都有一個(gè) bin。對(duì)于超過(guò) 512 字節(jié)(64 位為 1024 字節(jié))的塊,堆管理器改為使用“l(fā)arge bin”。
63 large bin中的每一個(gè)都與small bin的操作方式大致相同,但不是存儲(chǔ)固定大小的塊,而是存儲(chǔ)大小范圍內(nèi)的塊。每個(gè)large bin 的大小范圍都設(shè)計(jì)為不與small bin 的塊大小或其他large bin 的范圍重疊。換句話說(shuō),給定一個(gè)塊的大小,這個(gè)大小對(duì)應(yīng)的正好是一個(gè)small bin或large bin。
在這63個(gè)largebins中:第一組的32個(gè)largebin鏈依次以64字節(jié)步長(zhǎng)為間隔,即第一個(gè)largebin鏈中chunksize為1024-1087字節(jié),第二個(gè)large bin中chunk size為1088~1151字節(jié)。第二組的16個(gè)largebin鏈依次以512字節(jié)步長(zhǎng)為間隔;第三組的8個(gè)largebin鏈以步長(zhǎng)4096為間隔;第四組的4個(gè)largebin鏈以32768字節(jié)為間隔;第五組的2個(gè)largebin鏈以262144字節(jié)為間隔;最后一組的largebin鏈中的chunk大小無(wú)限制。
在進(jìn)行malloc操作的時(shí)候,首先確定用戶請(qǐng)求的大小屬于哪一個(gè)large bin,然后判斷該large bin中最大的chunk的size是否大于用戶請(qǐng)求的size。如果大于,就從尾開(kāi)始遍歷該large bin,找到第一個(gè)size相等或接近的chunk,分配給用戶。如果該chunk大于用戶請(qǐng)求的size的話,就將該chunk拆分為兩個(gè)chunk:前者返回給用戶,且size等同于用戶請(qǐng)求的size;剩余的部分做為一個(gè)新的chunk添加到unsorted bin中。
如果該large bin中最大的chunk的size小于用戶請(qǐng)求的size的話,那么就依次查看后續(xù)的large bin中是否有滿足需求的chunk,不過(guò)需要注意的是鑒于bin的個(gè)數(shù)較多(不同bin中的chunk極有可能在不同的內(nèi)存頁(yè)中),如果按照上一段中介紹的方法進(jìn)行遍歷的話(即遍歷每個(gè)bin中的chunk),就可能會(huì)發(fā)生多次內(nèi)存頁(yè)中斷操作,進(jìn)而嚴(yán)重影響檢索速度,所以glibc malloc設(shè)計(jì)了Binmap結(jié)構(gòu)體來(lái)幫助提高bin-by-bin檢索的速度。Binmap記錄了各個(gè)bin中是否為空,通過(guò)bitmap可以避免檢索一些空的bin。如果通過(guò)binmap找到了下一個(gè)非空的large bin的話,就按照上一段中的方法分配chunk,否則就使用top chunk(在后面有講)來(lái)分配合適的內(nèi)存。
large bin的free 操作與small bin一致,此處不再贅述。
上述幾種bin,組成了進(jìn)程中最核心的分配部分:bins,如下圖所示:
5.4 特殊chunk
上節(jié)內(nèi)容講述了幾種bin以及各種bin內(nèi)存的分配和釋放特點(diǎn),但是,僅僅上面幾種bin還不能夠滿足,比如假如上述bins不能滿足分配條件的時(shí)候,glibc提出了另外幾種特殊的chunk供分配和釋放,分別為top chunk,mmaped chunk 和last remainder chunk。
top chunk是堆最上面的一段空間,它不屬于任何bin,當(dāng)所有的bin都無(wú)法滿足分配要求時(shí),就要從這塊區(qū)域里來(lái)分配,分配的空間返給用戶,剩余部分形成新的top chunk,如果top chunk的空間也不滿足用戶的請(qǐng)求,就要使用brk或者mmap來(lái)向系統(tǒng)申請(qǐng)更多的堆空間(主分配區(qū)使用brk、sbrk,非主分配區(qū)使用mmap)。
在free chunk的時(shí)候,如果chunk size不屬于fastbin的范圍,就要考慮是不是和top chunk挨著,如果挨著,就要merge到top chunk中。
當(dāng)分配的內(nèi)存非常大(大于分配閥值,默認(rèn)128K)的時(shí)候,需要被mmap映射,則會(huì)放到mmaped chunk上,當(dāng)釋放mmaped chunk上的內(nèi)存的時(shí)候會(huì)直接交還給操作系統(tǒng)。 (chunk中的M標(biāo)志位置1)
Last remainder chunk是另外一種特殊的chunk,這個(gè)特殊chunk是被維護(hù)在unsorted bin中的。
如果用戶申請(qǐng)的size屬于small bin的,但是又不能精確匹配的情況下,這時(shí)候采用最佳匹配(比如申請(qǐng)128字節(jié),但是對(duì)應(yīng)的bin是空,只有256字節(jié)的bin非空,這時(shí)候就要從256字節(jié)的bin上分配),這樣會(huì)split chunk成兩部分,一部分返給用戶,另一部分形成last remainder chunk,插入到unsorted bin中。
當(dāng)需要分配一個(gè)small chunk,但在small bins中找不到合適的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk被分裂成兩個(gè)chunk,其中一個(gè)chunk返回給用戶,另一個(gè)chunk變成新的last remainder chunk。
last remainder chunk主要通過(guò)提高內(nèi)存分配的局部性來(lái)提高連續(xù)malloc(產(chǎn)生大量 small chunk)的效率。
5.5 chunk 切分
chunk釋放時(shí),其長(zhǎng)度不屬于fastbins的范圍,則合并前后相鄰的chunk。
首次分配的長(zhǎng)度在large bin的范圍,并且fast bins中有空閑chunk,則將fastbins中的chunk與相鄰空閑的chunk進(jìn)行合并,然后將合并后的chunk放到unsorted bin中,如果fastbin中的chunk相鄰的chunk并非空閑無(wú)法合并,仍舊將該chunk放到unsorted bin中,即能合并的話就進(jìn)行合并,但最終都會(huì)放到unsorted bin中。
fastbins,small bin中都沒(méi)有合適的chunk,top chunk的長(zhǎng)度也不能滿足需要,則對(duì)fast bin中的chunk進(jìn)行合并。
5.6 chunk 合并
前面講了相鄰的chunk可以合并成一個(gè)大的chunk,反過(guò)來(lái),一個(gè)大的chunk也可以分裂成兩個(gè)小的chunk。chunk的分裂與從top chunk中分配新的chunk是一樣的。需要注意的一點(diǎn)是:分裂后的兩個(gè)chunk其長(zhǎng)度必須均大于chunk的最小長(zhǎng)度(對(duì)于64位系統(tǒng)是32字節(jié)),即保證分裂后的兩個(gè)chunk仍舊是可以被分配使用的,否則則不進(jìn)行分裂,而是將整個(gè)chunk返回給用戶。
6 內(nèi)存分配(malloc)
glibc運(yùn)行時(shí)庫(kù)分配動(dòng)態(tài)內(nèi)存,底層用的是malloc來(lái)實(shí)現(xiàn)(new 最終也是調(diào)用malloc),下面是malloc函數(shù)調(diào)用流程圖:
在此,將上述流程圖以文字形式表示出來(lái),以方便大家理解:
獲取分配區(qū)的鎖,為了防止多個(gè)線程同時(shí)訪問(wèn)同一個(gè)分配區(qū),在進(jìn)行分配之前需要取得分配區(qū)域的鎖。線程先查看線程私有實(shí)例中是否已經(jīng)存在一個(gè)分配區(qū),如果存在嘗試對(duì)該分配區(qū)加鎖,如果加鎖成功,使用該分配區(qū)分配內(nèi)存,否則,該線程搜索分配區(qū)循環(huán)鏈表試圖獲得一個(gè)空閑(沒(méi)有加鎖)的分配區(qū)。如果所有的分配區(qū)都已經(jīng)加鎖,那么 ptmalloc 會(huì)開(kāi)辟一個(gè)新的分配區(qū),把該分配區(qū)加入到全局分配區(qū)循環(huán)鏈表和線程的私有實(shí)例中并加鎖,然后使用該分配區(qū)進(jìn)行分配操作。開(kāi)辟出來(lái)的新分配區(qū)一定為非主分配區(qū),因?yàn)橹鞣峙鋮^(qū)是從父進(jìn)程那里繼承來(lái)的。開(kāi)辟非主分配區(qū)時(shí)會(huì)調(diào)用 mmap()創(chuàng)建一個(gè) sub-heap,并設(shè)置好 top chunk。
將用戶的請(qǐng)求大小轉(zhuǎn)換為實(shí)際需要分配的 chunk 空間大小。
判斷所需分配chunk 的大小是否滿足chunk_size <= max_fast (max_fast 默認(rèn)為 64B), 如果是的話,則轉(zhuǎn)下一步,否則跳到第 5 步。
首先嘗試在 fast bins 中取一個(gè)所需大小的 chunk 分配給用戶。如果可以找到,則分配結(jié)束。否則轉(zhuǎn)到下一步。
判斷所需大小是否處在 small bins 中,即判斷 chunk_size < 512B 是否成立。如果chunk 大小處在 small bins 中,則轉(zhuǎn)下一步,否則轉(zhuǎn)到第 6 步。
根據(jù)所需分配的 chunk 的大小,找到具體所在的某個(gè) small bin,從該 bin 的尾部摘取一個(gè)恰好滿足大小的 chunk。若成功,則分配結(jié)束,否則,轉(zhuǎn)到下一步。
到了這一步,說(shuō)明需要分配的是一塊大的內(nèi)存,或者 small bins 中找不到合適的chunk。于是,ptmalloc 首先會(huì)遍歷 fast bins 中的 chunk,將相鄰的 chunk 進(jìn)行合并,并鏈接到 unsorted bin 中,然后遍歷 unsorted bin 中的 chunk,如果 unsorted bin 只有一個(gè) chunk,并且這個(gè) chunk 在上次分配時(shí)被使用過(guò),并且所需分配的 chunk 大小屬于 small bins,并且 chunk 的大小大于等于需要分配的大小,這種情況下就直接將該 chunk 進(jìn)行切割,分配結(jié)束,否則將根據(jù) chunk 的空間大小將其放入 small bins 或是 large bins 中,遍歷完成后,轉(zhuǎn)入下一步。
到了這一步,說(shuō)明需要分配的是一塊大的內(nèi)存,或者 small bins 和 unsorted bin 中都找不到合適的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干凈了。從 large bins 中按照“smallest-first,best-fit”原則,找一個(gè)合適的 chunk,從中劃分一塊所需大小的 chunk,并將剩下的部分鏈接回到 bins 中。若操作成功,則分配結(jié)束,否則轉(zhuǎn)到下一步。
如果搜索 fast bins 和 bins 都沒(méi)有找到合適的 chunk,那么就需要操作 top chunk 來(lái)進(jìn)行分配了。判斷 top chunk 大小是否滿足所需 chunk 的大小,如果是,則從 top chunk 中分出一塊來(lái)。否則轉(zhuǎn)到下一步。
到了這一步,說(shuō)明 top chunk 也不能滿足分配要求,所以,于是就有了兩個(gè)選擇: 如果是主分配區(qū),調(diào)用 sbrk(),增加 top chunk 大小;如果是非主分配區(qū),調(diào)用 mmap 來(lái)分配一個(gè)新的 sub-heap,增加 top chunk 大小;或者使用 mmap()來(lái)直接分配。在這里,需要依靠 chunk 的大小來(lái)決定到底使用哪種方法。判斷所需分配的 chunk 大小是否大于等于 mmap 分配閾值,如果是的話,則轉(zhuǎn)下一步,調(diào)用 mmap 分配, 否則跳到第 12 步,增加 top chunk 的大小。
使用 mmap 系統(tǒng)調(diào)用為程序的內(nèi)存空間映射一塊 chunk_size align 4kB 大小的空間。然后將內(nèi)存指針?lè)祷亟o用戶。
判斷是否為第一次調(diào)用 malloc,若是主分配區(qū),則需要進(jìn)行一次初始化工作,分配一塊大小為(chunk_size + 128KB) align 4KB 大小的空間作為初始的 heap。若已經(jīng)初始化過(guò)了,主分配區(qū)則調(diào)用 sbrk()增加 heap 空間,分主分配區(qū)則在 top chunk 中切割出一個(gè) chunk,使之滿足分配需求,并將內(nèi)存指針?lè)祷亟o用戶。
將上面流程串起來(lái)就是:
根據(jù)用戶請(qǐng)求分配的內(nèi)存的大小,ptmalloc有可能會(huì)在兩個(gè)地方為用戶分配內(nèi)存空間。在第一次分配內(nèi)存時(shí),一般情況下只存在一個(gè)主分配區(qū),但也有可能從父進(jìn)程那里繼承來(lái)了多個(gè)非主分配區(qū),在這里主要討論主分配區(qū)的情況,brk值等于start_brk,所以實(shí)際上heap大小為0,top chunk 大小也是0。這時(shí),如果不增加heap大小,就不能滿足任何分配要求。所以,若用戶的請(qǐng)求的內(nèi)存大小小于mmap分配閾值, 則ptmalloc會(huì)初始heap。然后在heap中分配空間給用戶,以后的分配就基于這個(gè)heap進(jìn)行。若第一次用戶的請(qǐng)求就大于mmap分配閾值,則ptmalloc直接使用mmap()分配一塊內(nèi)存給用戶,而heap也就沒(méi)有被初始化,直到用戶第一次請(qǐng)求小于mmap分配閾值的內(nèi)存分配。第一次以后的分配就比較復(fù)雜了,簡(jiǎn)單說(shuō)來(lái),ptmalloc首先會(huì)查找fast bins,如果不能找到匹配的chunk,則查找small bins。若仍然不滿足要求,則合并fast bins,把chunk加入unsorted bin,在unsorted bin中查找,若仍然不滿足要求,把unsorted bin 中的chunk全加入large bins 中,并查找large bins。在fast bins 和small bins中的查找都需要精確匹配, 而在large bins中查找時(shí),則遵循“smallest-first,best-fit”的原則,不需要精確匹配。若以上方法都失敗了,則ptmalloc會(huì)考慮使用top chunk。若top chunk也不能滿足分配要求。而且所需chunk大小大于mmap分配閾值,則使用mmap進(jìn)行分配。否則增加heap,增大top chunk。以滿足分配要求。
當(dāng)然了,glibc中malloc的分配遠(yuǎn)比上面的要復(fù)雜的多,要考慮到各種情況,比如指針異常越界等,將這些判斷條件也加入到流程圖中,如下圖所示:
7 內(nèi)存釋放(free)
malloc進(jìn)行內(nèi)存分配,那么與malloc相對(duì)的就是free,進(jìn)行內(nèi)存釋放,下面是free函數(shù)的基本流程圖:
對(duì)上述流程圖進(jìn)行描述,如下:
獲取分配區(qū)的鎖,保證線程安全。
如果free的是空指針,則返回,什么都不做。
判斷當(dāng)前chunk是否是mmap映射區(qū)域映射的內(nèi)存,如果是,則直接munmap()釋放這塊內(nèi)存。前面的已使用chunk的數(shù)據(jù)結(jié)構(gòu)中,我們可以看到有M來(lái)標(biāo)識(shí)是否是mmap映射的內(nèi)存。
判斷chunk是否與top chunk相鄰,如果相鄰,則直接和top chunk合并(和top chunk相鄰相當(dāng)于和分配區(qū)中的空閑內(nèi)存塊相鄰)。否則,轉(zhuǎn)到步驟8
如果chunk的大小大于max_fast(64b),則放入unsorted bin,并且檢查是否有合并,有合并情況并且和top chunk相鄰,則轉(zhuǎn)到步驟8;沒(méi)有合并情況則free。
如果chunk的大小小于 max_fast(64b),則直接放入fast bin,fast bin并沒(méi)有改變chunk的狀態(tài)。沒(méi)有合并情況,則free;有合并情況,轉(zhuǎn)到步驟7
在fast bin,如果當(dāng)前chunk的下一個(gè)chunk也是空閑的,則將這兩個(gè)chunk合并,放入unsorted bin上面。合并后的大小如果大于64B,會(huì)觸發(fā)進(jìn)行fast bins的合并操作,fast bins中的chunk將被遍歷,并與相鄰的空閑chunk進(jìn)行合并,合并后的chunk會(huì)被放到unsorted bin中,fast bin會(huì)變?yōu)榭铡:喜⒑蟮腸hunk和topchunk相鄰,則會(huì)合并到topchunk中。轉(zhuǎn)到步驟8
判斷top chunk的大小是否大于mmap收縮閾值(默認(rèn)為128KB),如果是的話,對(duì)于主分配區(qū),則會(huì)試圖歸還top chunk中的一部分給操作系統(tǒng)。free結(jié)束。
如果將free函數(shù)內(nèi)部各種條件加入進(jìn)去,那么free調(diào)用的詳細(xì)流程圖如下:
8 問(wèn)題分析以及解決
通過(guò)前面對(duì)glibc運(yùn)行時(shí)庫(kù)的分析,基本就能定位出原因,是因?yàn)槲覀冋{(diào)用了free進(jìn)行釋放,但僅僅是將內(nèi)存返還給了glibc庫(kù),而glibc庫(kù)卻沒(méi)有將內(nèi)存歸還操作系統(tǒng),最終導(dǎo)致系統(tǒng)內(nèi)存耗盡,程序因?yàn)?OOM 被系統(tǒng)殺掉。
有以下兩種方案:
禁用 ptmalloc 的 mmap 分配 閾 值 動(dòng) 態(tài) 調(diào) 整 機(jī) 制 。 通 過(guò) mallopt() 設(shè)置M_TRIM_THRESHOLD,M_MMAP_THRESHOLD,M_TOP_PAD 和 M_MMAP_MAX 中的任意一個(gè),關(guān)閉 mmap 分配閾值動(dòng)態(tài)調(diào)整機(jī)制,同時(shí)需要將 mmap 分配閾值設(shè)置為 64K,大于 64K 的內(nèi)存分配都使用mmap 向系統(tǒng)分配,釋放大于 64K 的內(nèi)存將調(diào)用 munmap 釋放回系統(tǒng)。但是,這種方案的 缺點(diǎn)是每次內(nèi)存分配和申請(qǐng),都是直接向操作系統(tǒng)申請(qǐng),效率低。
預(yù) 估 程 序 可 以 使 用 的 最 大 物 理 內(nèi) 存 大 小 , 配 置 系 統(tǒng) 的
/proc/sys/vm/overcommit_memory,/proc/sys/vm/overcommit_ratio,以及使用 ulimt –v限制程序能使用虛擬內(nèi)存空間大小,防止程序因 OOM 被殺掉。這種方案的 缺點(diǎn)是如果預(yù)估的內(nèi)存小于進(jìn)程實(shí)際占用,那么仍然會(huì)出現(xiàn)OOM,導(dǎo)致進(jìn)程被殺掉。
tcmalloc
最終采用tcmalloc來(lái)解決了問(wèn)題,后面有機(jī)會(huì)的話,會(huì)寫(xiě)一篇tcmalloc內(nèi)存管理的相關(guān)文章。
9 結(jié)語(yǔ)
業(yè)界語(yǔ)句說(shuō)法,是否了解內(nèi)存管理機(jī)制,是辨別C/C++程序員和其他的高級(jí)語(yǔ)言程序員的重要區(qū)別。作為C/C++中的最重要的特性,指針及動(dòng)態(tài)內(nèi)存管理在給編程帶來(lái)極大的靈活性的同時(shí),也給開(kāi)發(fā)人員帶來(lái)了許多困擾。
了解底層內(nèi)存實(shí)現(xiàn),有時(shí)候會(huì)有意想不到的效果哦。
先寫(xiě)到這里吧。
本文提供了PDF版本,關(guān)注本公眾號(hào),后臺(tái)回復(fù)[malloc]獲取,另外還有批量電子書(shū),后臺(tái)回復(fù)[pdf]免費(fèi)獲取。
10 參考
1、https://sourceware.org/glibc/wiki/MallocInternals
2、https://titanwolf.org/Network/Articles/Article?AID=99524c69-cb90-4d61-bb28-01c0864d0ccc
3、https://blog.fearcat.in/a?ID=00100-535db575-0d98-4287-92b6-4d7d9604b216
4、https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/
5、https://sploitfun.wordpress.com/2015/02/11/syscalls-used-by-malloc
Linux 任務(wù)調(diào)度
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。