2. 標記結構
本章節將介紹基本的內存標記結構,包括chunk, tree chunk, sbin, tbin, segment, mstate等.這些重要的機構組成了dlmalloc分配算法的基礎.算法
2.1 chunk
chunk是dlmalloc中最基本的一種結構,它表明了一塊通過劃分後被管理的內存單元. dlmalloc全部對內存的操做幾乎都聚焦在chunk上.須要注意的是, chunk雖然看似基礎,但不表明它能管理的內存小.事實上chunk被劃分爲兩種類型,小於256字節的稱爲small chunk,而大於等於256字節的被稱爲tree chunk.緩存
2.1.1 chunk佈局
在dlmalloc的源碼中有一幅用字符畫成的示意圖,套用Doug Lea本人的話說 「is misleading but accurate and necessary」.初次接觸這種結構劃分會以爲既古怪又彆扭. 所以,接下來的幾節內容須要認真體會,讓大腦適應這種錯位的思考.數據結構
對於一個已分配chunk, 它在內存中多是這個樣子,多線程

上圖爲了便於理解特地使用了兩種顏色來描述,藍色區域的部分表明當前的已分配chunk,而黃色區域表明上一個和下一個chunk的部分區域.app
解釋一下其中的含義, 從藍色區域開始看, 在已分配chunk的開始記錄當前chunk的size信息,同時在最後兩個bit位分別記錄當前chunk和前一個chunk是否被使用,簡稱爲C和P兩個bit位.之因此能夠作到這一點,是由於在dlmalloc中全部的chunk size至少對齊到大於8並以2爲底的指數邊界上.這樣,至少最後3位都是0,所以這些多餘的bit位就能夠拿來記錄更多的信息. size信息後面是payload部分,顯然,當前chunk的使用信息也會記錄在下一個chunk開始的P位上.less
而對於一個空閒chunk, 其內存佈局應該是這樣的,函數

與以前的一致, 藍色區域表明當前空閒chunk, 黃色區域表明相鄰的先後chunk.能夠看到空閒chunk與以前的區別在於開始的size後多了next和prev指針,它們把具備相同大小的空閒chunk連接到一塊兒.另外一個區別是,在空閒chunk最後一樣記錄該chunk的大小信息.那麼爲何一樣的信息要記錄兩次呢?在下一個小節中會解釋這個問題.佈局
另外, 還有一點須要說明的是,空閒chunk的相鄰chunk必然是已分配的chunk.由於若是存在相鄰兩個chunk都是空閒的,那麼dlmalloc會把它們合併爲一個更大的chunk.post
2.1.2 邊界標記法(Boundary Tag)
上一節中介紹的chunk佈局其實只是理論上的東西,而實現它使用了被稱爲邊界標記法 (boundary tag)的技術.據做者說,此方法最先是大神Knuth提出的.實現邊界標記法使用了名爲malloc_chunk的結構體,它的定義以下,測試

該結構體(之後簡稱mchunk)由四個field組成.最開始是prev_foot,記錄了上一個鄰接chunk的最後4個字節.接下來是head,記錄當前chunk的size以及C和P位.最後兩個是fd, bk指針,只對空閒chunk起做用,用於連接相同大小的空閒chunk.
爲了不讀者感到困惑, 在上一節的圖中並無畫出對應的mchunk, 如今補充完整以下,

上圖用不一樣顏色畫了幾個連續交錯的chunk,並故意在中間斷開,標註出mchunk以及chunk的實際範圍,由此獲得以下的結論,
1. mchunk的做用是將連續內存劃分爲一段段小的區塊, 並在內部保留這些區塊的信息.
2. mchunk最後的fd, bk是能夠複用的,對於空閒chunk它們表明連接指針,而已使用chunk中這兩個field實際上存放的是payload數據.
3. mchunk與chunk不是一個等同的概念.這一點是容易讓人混淆和困惑的. mchunk只是一個邊界信息,它實際上橫跨了兩個相鄰chunk.儘管通常認爲mchunk能夠指代當前的chunk,由於你能夠從它推算出想要的地址.但從邏輯上,它既不表明當前chunk也不表明prev chunk.
4. prev_foot字段一樣是一個可複用的字段. 通常狀況下它有三種含義,若是前一個相鄰chunk是空閒狀態,它記錄該chunk的大小(圖中mchunk C).其目的就是你能夠很方便的從當前chunk得到上一個相鄰chunk的首地址,從而快速對它們進行合併.這也就是上一小節介紹的空閒chunk會在head和footer存在兩個chunk size的緣由,位於footer的size實際上保存在下一個mchunk中.若是前一個相鄰chunk處於in used狀態,那麼該字段可能有兩種狀況.一種狀況是FOOTERS宏爲1,這時它保存一個交叉檢查值,用於在free()的時候進行校驗.若是該宏爲0,則它沒有多餘的含義,單純只是前面chunk的payload數據.
5. 最後還有一點須要注意的是, 儘管前面提到全部chunk size都是對齊到至少等於8的2的指數.但這不意味着mchunk就對齊到這一邊界上,由於如前所述mchunk和chunk是兩碼事.我本人最先在看這部分代碼時曾天真的覺得mchunk也是對齊的,結果到後面竟然徹底看不懂,後來才發現這傢伙根本沒有對齊.
到目前爲止, 已經瞭解了dlmalloc的邊界標記法是如何實現的.可能有人會對此感到不覺得然,做者爲何要把代碼寫成這個樣子呢?其實,要理解這個意圖請換位思考一下,若是實現一個相似的東西,你會怎麼寫呢?人們很快會發現,不按照這種方式設計,想要達到一樣目的幾乎是很困難的.由於一個chunk在頭和尾存在head和footer(也可能不存在footer),中間是一片長度沒法肯定的區域.假設按照正常從頭至尾的順序寫,這個結構體中間填充多長的內容是無法肯定的.所以, Doug Lea巧妙的把除了payload以外的overhead部分合並在一塊兒,做爲boundary tag來分割整個內存區域,既能作到很方便的計算,又準確地表達了chunk的結構.
2.1.3 chunk操做
針對mchunk的設計,源碼中定義了大量的宏來對其進行操做.因爲數量不少加上嵌套,對於閱讀源碼會形成不小的障礙,所以這裏會對一些經常使用宏進行梳理和簡單講解.

定義size_t的大小和bit位數.注意, dlmalloc中對size_t規定爲必須爲無符號類型,且與指針類型(void*)等寬度.所以若是目標平臺還在使用有符號的size_t,只能使用較舊版本的dlmalloc.

常量定義, 這裏使用size_t做爲強制類型是爲了不在某些平臺上出現錯誤.


對齊邊界和對齊掩碼, 邊界默認定義爲兩倍指針寬度, 固然還能夠修改的更大,但必須以2爲底的指數.有關對齊掩碼相關知識請參考1.4.1節.

判斷給定地址是否對齊在邊界上,原理一樣在1.4.1節中解釋,再也不贅述.

計算給定地址須要對齊的偏移量,請參考1.4.3節中的介紹.

chunk size(其實是mchunk size,後面再也不特地說明)以及overhead大小.注意,若是FOOTERS等於1, overhead將多出4個字節,其實就是在chunk最後放置交叉檢查項多出的負載.另外,無論是否有FOOTERS, chunk size自己是不變的.

最小chunk大小. dlmalloc中即便調用malloc(0)也是會分配內存的.這種狀況下徹底不考慮用戶payload了,僅僅將chunk size作了對齊.該值的意義在於,這是dlmalloc中除了mmap chunk之外的最壞負載損耗.

這是兩個最經常使用的宏之一, 在chunk和用戶地址以前切換.

該宏的意思是, 將指定chunk的用戶地址對齊,返回對齊後的新chunk地址.理解很容易,從chunk獲取用戶地址,計算對齊偏移,再移動chunk指針.從這裏能夠看到, chunk中對齊的並不是是mchunk指針,而是用戶地址.

將用戶請求的內存值轉化成實際的內部大小.該宏也是經常使用宏之一,請求大小先加上overhead(根據FOOTERS而有所不一樣),再對齊到邊界上.

該宏是對上面一系列宏的封裝, 若是用戶請求小於最小請求, 直接使用最小chunk大小,不然認爲正常,轉化成內部可用大小.

上面這一組定義了C位和P位,其中FLAG4_BIT並無實際用途,只是做爲將來的擴展.

測試C和P位,分別用掩碼作與運算.着重說一下最後兩個判斷. is_inuse是用C和P的掩碼分別測試,而後與P的掩碼對比.有人會問這個直接用前面的cinuse不就行了嗎?由於存在一種特殊狀況,就是被mmap出來的chunk是不見得有鄰接chunk的,因此其C和P都被置0. is_inuse就是爲了判斷包括mmap chunk在內的全部類型chunk的使用狀況.理解了這個, is_mmapped就很容易理解了.

計算指定chunk的大小,清除最後3bit後獲得的就是size.

對P位的操做.

這是兩個計算偏移量的宏, 返回結果被認爲是mchunk, 通常用做chunk切割.

這兩個也是很是重要的宏. 分別用來計算前一個或後一個鄰接chunk的首地址. next_chunk先獲取當前chunk的size做爲偏移量,再移動chunk指針. prev_chunk則是直接加上前一個chunk的footer做爲偏移量.須要注意的是, prev_chunk僅僅適用於前一個鄰接chunk爲空閒塊的狀況.如前所述,非空閒chunk這裏放置的多是用戶數據或者交叉檢查項.實際上這個宏就是用於free時試圖與前一個free chunk合併時的判斷.

獲取下一個chunk的P位,應該與當前chunk的C是一致的.該宏通常用做交叉檢查項.

獲取和設置當前chunk的footer,由於footer被放在下一個鄰接chunk的prev_foot中,所以須要加上size做爲偏移量.

該宏是個複合操做, 用於對free chunk進行設置.由於free chunk在head和footer都保存其size,因此首先將其head中寫入size.又由於free chunk的P必定是1(不然會與前面合併),所以還須要與P掩碼進行一次位與.接着,利用前面的set_foot宏在footer中一樣寫入size.

這個宏與上面的區別在於多了一個next chunk的P位維護.由於是針對free chunk, next chunk的P須要置0.
2.1.4 tree chunk
以前介紹的chunk其實都屬於小型chunk,而較大型的chunk是經過名爲malloc_tree_chunk的結構體來描述的.

同mchunk相似tree chunk(之後簡稱tchunk)開始的field是如出一轍的,這就保證了兩種類型在基礎結構上具備較好的兼容性,下降了代碼編寫難度. tchunk與前者的區別在於管理上使用不一樣的結構. mchunk是經過雙鏈表組織起來的,而tchunk使用了一種樹形結構,在介紹分箱時會詳細說明.所以tchunk中child分別表明了左右子樹節點, parent表明父節點, index表明該chunk所在分箱的編號.而fd, bk與mchunk一致,都是用於連接相同size的chunk.另外,須要說明的是,上述結構體字段一樣只適用於free chunk,在used chunk上它們都是可複用的
=======================================================================================
咱們寫過不少C程序了,常常會分配內存。記得剛學C語言時老師說過,能夠向兩個地方申請內存:一個是棧、一個是堆。小塊內存向棧申請,函數調用結束後程序會自動釋放內存。大塊內存向堆申請,記得必定要本身釋放,不然會形成內存泄漏。向堆申請內存直接調用malloc()就能夠了,參數是你申請的內存量。釋放內存時直接調用free()就能夠了,參數是內存塊指針。
看似平靜的海面,海底則波濤洶涌。當時尚未學操做系統原理,更沒有讀過Linux內核代碼。如今仔細想一想才發現申請動態內存是一件多麼麻煩的事情。動態內存管理涉及到兩個層面的問題:內核層面和用戶層面。系統中的內存如何管理這是內核考慮的事情,總不能讓應用程序隨便使用系統中的內存吧。內核嚮應用程序提供了接口(爲此Linux提供了兩個系統調用brk和mmap),當應用程序須要申請內存時向內核提出請求,內核查找並分配一塊可用內存供應用程序使用。這部份內容屬於內核範疇,不屬於C基礎庫,所以不深刻說了。那麼用戶層面作什麼呢?用戶層面須要合理管理內存申請和釋放請求。好比:brk()能夠擴充或收縮堆的大小,你總不能每分配一次內存就調用一次brk()吧?釋放內存時更麻煩,你必須保證內存塊的釋放順序。好比先申請了內存塊a,而後申請了內存塊b,而後釋放a(b仍然在使用),若是釋放a時調用了brk()就會出問題。你不能在使用b的同時釋放a。
好在出現了一個叫作「內存分配器」的東西,內存分配器接管了應用程序申請內存和釋放內存的請求,應用程序不再須要直接調用brk()和mmap()了,而是向內存分配器提交申請。有了內存分配器,咱們只須要記住malloc()和free()兩個接口函數就能夠了,其餘繁雜事情所有交給內存分配器負責了。申請內存時,內存分配器會一次向內核申請大量內存,而後分批交給應用程序,從而提升了效率。釋放內存時,應用程序也是將內存釋放給內存分配器,內存分配器在合適的時候再將內存釋放會內核。
dlmalloc就是一種內存分配器,由Doug Lea在1987年開發完成,這是Android系統中使用的內存分配器。而Linux系統中採用的是ptmalloc,ptmalloc在dlmalloc的基礎上進行了改進,以更好適應多線程。dlmalloc採用兩種方式申請內存,若是應用程序單次申請的內存量小於256kb,dlmalloc調用brk()擴展進程堆空間,可是dlmalloc向內核申請的內存量大於應用程序申請的內存量,申請到內存後dlmalloc將內存分紅兩塊,一塊返回給應用程序,另外一塊做爲空閒內存先保留起來。下次應用程序申請內存時dlmalloc就不須要向內核申請內存了,從而加快內存分配效率。當應用程序調用free()釋放內存時,若是內存塊小於256kb,dlmalloc並不立刻將內存塊釋放回內存,而是將內存塊標記爲空閒狀態。這麼作的緣由有兩個:一是內存塊不必定能立刻釋放會內核(好比內存塊不是位於堆頂端),二是供應用程序下次申請內存使用(這是主要緣由)。當dlmalloc中空閒內存量達到必定值時dlmalloc纔將空閒內存釋放會內核。若是應用程序申請的內存大於256kb,dlmalloc調用mmap()向內核申請一塊內存,返回返還給應用程序使用。若是應用程序釋放的內存大於256kb,dlmalloc立刻調用munmap()釋放內存。dlmalloc不會緩存大於256kb的內存塊,由於這樣的內存塊太大了,最好不要長期佔用這麼大的內存資源。
dlmalloc中,申請到的內存被分割成若干個內存塊,dlmalloc採用兩種不一樣的數據結構表示這些內存塊。小內存塊保存在鏈表中,用struct malloc_chunk表示;大內存塊保存在樹形結構中,用struct malloc_tree_chunk表示。struct malloc_chunk結構以下:
[cpp] view plaincopy