之前寫過關於內存管理的幾篇文章, 但是比較零碎, 網上很多文章又偏於理論或者局限一塊內容, 少有一個系列的分析. 一直想自己寫個, 正好借助這次培訓機會寫篇文章, 從源碼分析申請內存之后到實際訪問內存之間系統究竟做了什么, 探討一下源碼作者如此設計內存管理模塊的目的與意義.
暫時規划分四部分完成:
glibc堆內存管理
內核如何管理虛擬地址空間
虛擬內存與物理內存的映射
內核如何處理內存地址異常
本文將以malloc()函數作為入口, 首先分析glibc如何管理動態內存申請與釋放, 以此理解一個通用內存管理工具的設計目標. 然后從malloc()調用的系統調用入手, 理解內核如何管理虛擬地址空間的. 第三部分討論虛擬地址空間與物理頁表的映射關系. 最后分析下內核如何處理內存地址異常.
PS: 從源碼入手分析往往只見樹木不見森林, 更多的相關知識還需要讀者自己學習了.
PPS: 源碼版本ptmalloc3 / linux-3.10.
PS3: 內核代碼與架構強相關, 本文默認以32bit ARMv7為平台分析, 中間可能會摻雜64bit x86分析.
PSP: 想到再說.
正文開始.
glibc的內存管理
glibc使用ptmalloc(最早由Doug Lea實現的dlmalloc經Wolfram Gloger優化多線程而來)做為內存管理工具. 目前ptmalloc最新版為ptmalloc3(2006.5.31), 本文基於此版本分析其實現(主要分析dlmalloc), 源碼見http://www.malloc.de/en/, 其中malloc.c即dlmalloc實現,ptmalloc.c為dlmalloc.c的多線程封裝.
ptmalloc源碼並不復雜, 內存管理僅一個文件約五千行, 其中一半多是注釋, 網上還有一堆介紹. 但思考作者設計數據結構與算法的目的是理解代碼的最終目的, 換言之思考實現一個快速高效的通用內存管理工具的目標有哪些:
1. 能夠應對不同大小內存分配請求(通用)
2. 能夠快速分配釋放以及回收內存(快速)
3. 盡可能減少管理開銷(高效)
3. 其它特點, 如對不同平台兼容性, 內存越界檢查, 保留內存供異常時使用等等
在我司代碼中也有許多技巧來實現以上目標, 比如預留靜態內存, 預留內存分片, 未分配內存首部做雙向鏈表等等. 然而我司的內存管理目標是穩定的碼流, 其特點是每次內存請求大小較平均, 一般不會出現特別小如四字節這種情況, 同時前后端穩定收發流, 較早申請的內存也較早釋放, 索引內存往往是順序的, 管理內存塊僅需鏈表即可. 反觀glibc, 首先其申請的大小是不限定的, 可能零字節可能上千兆, 分配與釋放的順序也是亂序的, 可能先分配的后釋放, 也可能反過來. 因此設計glibc內存管理器要復雜的多: 首先針對第一點需要區分申請的目標, 對於幾個字節與幾兆的內存申請肯定不能使用同一內存分配器, 第二設計合理的數據結構來根據一個長度索引最合適的空閑內存或根據一個指針查找一個已分配的內存塊, 合理的縮減管理頭部, 降低額外開銷(第三點).
我們首先從dlmalloc的數據結構入手, 分析作者使用何種方式來達成以上目標, 然后我們再分析malloc()函數的實現, 理解glibc究竟是如何管理內存的. 另外提一句評價內存管理工具是否有效, 看的是該工具是否滿足使用需求而不是是否完備. 舉例而言, dlmalloc雖然很通用可是在分配我司的碼流緩存時顯然不及我司的內存管理器, 因其大段代碼都是無效的, 而有效的代碼分配效率也不如我司代碼, 因此還是要根據業務需求設計模型.
在分析數據結構之前, 我們先來看下作者的說明(在malloc.c文件首部), 可以更好的幫助我們理解代碼.
1. 支持指針與size_t互相轉換: size_t必須是無符號類型且位寬與指針一致(4字節或8字節), 否則只能使用較早的2.7.2版本.
2. 對齊(alignment)要求: 默認8字節對齊, 當前大部分機器與C編譯器都滿足該需求. 但是你也可以定義MALLOC_ALIGNMENT來擴大對齊(最大128字節), 后面我們會發現它必須是8的倍數的原因是因為第三位被用作標記位.
3. 每個內存塊(chunk)的最小前綴(overhead): 4或8字節(size_t為4字節), 8或16字節(size_t為8字節). 每個已分配的內存塊有一個字長的隱藏前綴用來保存長度及狀態信息. 如果定義了FOOTERS則還有一個字長的交叉檢查標記.
4. 每個內存塊的最小分配長度(包括前綴): 16字節(4字節指針), 32字節(8字節指針). 即使請求0字節(malloc(0))也會分配的空間, 后面我們會發現這個長度實際是sizeof(malloc_chunk). 通常情況下最大前綴浪費(即實際分配減去申請長度)小於等於最小分配長度, 但有個例外是當申請長度大於等於mmap_threshold, 轉而使用mmap()申請時會浪費系統頁的剩余部分(舉例申請一個內存頁, 加上管理前綴卻申請了兩個頁).
5. 安全: 指抵擋惡意代碼故意出錯的能力(比如釋放當前未分配的空間或覆蓋前一內存塊的尾部). 目前可以保證不修改任何內存地址低於堆起始地址的空間, 程序還能偵測部分不合適的free()與realloc(). 如果定義FOOTERS非零則每個已分配的內存塊會額外攜帶一個檢查字來確保它被正確分配. 默認情況下偵測到錯誤會導致程序abort(), 可以定義PROCEED_ON_ERROR來修改錯誤處理機制.
6. 線程安全: 除非定義USE_LOCKS否則不是線程安全的. 定義USE_LOCKS后, 每次調用malloc, free等都會調用pthread mutex或spinlock(win32). 這會降低運行速度, 可能會成為一個瓶頸. 如果你需要在同步環境中使用malloc, 建議考慮nedmalloc或ptmalloc.
系統需求: 只要定義MORECORE和/或MMAP/MUNMAP宏即可. dlmalloc可以通過unix sbrk或其它模擬機制(通過定義CALL_MORECORE)和/或mmap/munmap或其它模擬機制(通過定義CALL_MMAP/CALL_MUNMAP)來申請與釋放系統內存. 在大部分unix系統上它傾向於同時使用兩者. 在win32平台上它使用基於VirtualAlloc的模擬機制.
7. 算法概述: 大多數情況下dlmalloc是一個最適合選擇分配器. 對於給定請求通常它會選擇已存在的最合適的內存塊, 使用LRU順序(使用該策略減少分片). 對於小於256字節的請求, 如果不存在恰好合適的內存塊, 則偏向使用(大小)鄰近的內存塊, 使用MRU順序. 對於大於256字節的請求依賴與系統內存映射的策略.
8. MSPACES: 如果定義MSPACES, 則除malloc, free外還存在一組mspace_malloc, mspace_free接口. 這組接口會額外接收一個mspace參數, 該參數通過create_mspace接口生成. 如果定義ONLY_MSPACES則只有該組接口會被編譯. 可以用來創建線程局部分配器.
9. 編譯宏: 太多了, 略.
廢話完了, 讓我們開始正文. 首先為了記錄一個已分配的內存塊我們必須要設計一個內存塊管理結構, 在dlmalloc中即malloc_chunk. 當一個內存塊被分配時, 該結構也會與請求內存一起分配且該結構在申請內存的前部(因此也稱overhead).
1 struct malloc_chunk { 2 size_t prev_foot; //記錄前一塊內存塊的長度與狀態 3 size_t head; //當前塊的長度與狀態 4 struct malloc_chunk* fd; //同一大小的內存塊的雙向鏈表的前驅 5 struct malloc_chunk* bk; //同一大小的內存塊的雙向鏈表的后驅 6 }; 7 typedef struct malloc_chunk mchunk; 8 typedef struct malloc_chunk* mchunkptr; 9 typedef struct malloc_chunk* sbinptr;
這是一個令人迷惑的數據結構, 因為其設計結構與實際內存布局並不一致, 在代碼中計算偏移時常常讓人困惑. 所以我們先從最初模型開始分析: 一個內存塊的管理結構需要什么成員? 首先我們需要一個長度字段記錄它的長度(否則free時僅憑一個指針我們無法完成釋放內存的工作), 如果該內存塊是空閑的我們需要使用雙向鏈表將它管理起來. 另外為了盡可能減少內存碎片我們需要合並空閑內存, 對於當前內存塊的后一塊起始地址我們是已知的, 然而前一塊起始地址我們是未知的, 所以還需要記錄前一內存塊的長度(與狀態). 因此我們需要4個size_t空間存放這些信息. 這是一個非常大的開銷, 在32bit平台上即16個字節, 對於一次16字節的請求實際需要分配32字節, 如何降低管理結構的開銷? 首先考慮的是時分復用: 管理結構中的雙向鏈表僅在未分配時起效, 當內存被分配后脫離鏈表管理, 因此這兩個指針實際無需分配空間. 同理我們再考慮是否需要前一塊內存的長度呢? 當且僅當前一塊內存為空閑時我們才會需要合並內存, 因此當前塊內存無需記錄前一塊內存的長度(由前一塊內存自己在自己尾部記錄, 如果它是空閑內存的話), 只需記錄前一塊內存是否是空閑的. 因此malloc_chunk結構中的prev_foot實際是前一塊內存塊的空間, 而fd與bk指針則是當前塊的空間, 真正分配的管理字段僅head, 所以前文指出最小管理前綴開銷為sizeof(size_t). 這里還需注意的是prev_foot與head字段不僅僅保存長度還保存塊狀態. 另外空閑內存塊的前后必定是非空閑內存塊, 否則在內存塊釋放時必然會合並. 如之前所述, 當前塊必須知道前一塊是否空閑才能決定prev_foot值是否可信, 因此需要標記內存塊的狀態. 恰好內存塊都是以8字節對齊的, 因此低3位可以用來做為標記位. 其中head字段最低位為pinuse, 指示前一塊的狀態, 當其置零時前一塊內存塊為空閑狀態, prev_foot包含了前一塊內存塊的長度, 當其置位時表明前一塊內存塊已被使用, 無法獲取前一塊內存塊的長度. 通常第一個內存塊的pinuse都是置位的, 防止訪問不存在的內存. 第二位為cinuse, 指示當前塊的狀態, 該位主要用於(在free與realloc時)內部檢查. 每個剛分配的內存塊的cinuse與pinuse位均應該置位. prev_foot字段最低位代表map標記. 以上規則有個例外是mmap申請的大塊內存pinuse不會置位且其prev_foot中mmap標記必須置位, 因為mmap的內存往往是單獨申請, 需要自己攜帶prev_foot(用來記錄它在mmap區域中的偏移), 每個mmap的內存塊后還會跟着下一個內存塊管理結構的前兩個成員(保證通過檢查).
可以看到malloc_chunk結構的前兩個成員作用是以O(1)復雜度實現釋放內存操作, 而鏈表的作用是加快內存申請. 以上這種邊界標記的方式最早由高德納提出, 可以參見ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps了解相關技巧.
對於malloc_chunk結構而言, 其雙向鏈表中必須只能包含同一大小內存塊, 否則在索引鏈表時存在不同長度的內存塊必然導致低下的分配效率. 然而為每一個長度設計一個鏈表顯然也不合理. 因此dlmalloc又引入了另一個內存塊管理結構, malloc_tree_chunk.
1 //前四字節與malloc_chunk一致以保證相互轉化 2 struct malloc_tree_chunk { 3 size_t prev_foot; 4 size_t head; 5 struct malloc_tree_chunk* fd; 6 struct malloc_tree_chunk* bk; 7 struct malloc_tree_chunk* child[2]; 8 struct malloc_tree_chunk* parent; 9 bindex_t index; 10 }; 11 typedef struct malloc_tree_chunk tchunk; 12 typedef struct malloc_tree_chunk* tchunkptr; 13 typedef struct malloc_tree_chunk* tbinptr;
malloc_tree_chunk與malloc_chunk的不同之處在於它引入了二叉樹. 樹上的每一個節點代表一種長度的空閑內存塊, 同一長度的內存塊仍然通過雙向鏈表鏈接起來, 只有最早的內存塊(也是下一個被使用的, FIFO順序)會加入二叉樹中(該內存塊是否在樹中通過parent指針是否為空判斷). 理論上一棵樹即可管理所有不同大小內存塊, 但出於加速內存分配的考慮, 實際有多顆樹分別管理以2的n次冪的長度區間的內存塊(這塊下文再詳述). 每顆樹的左右子樹分別管理區間的左右部分, 即右子樹上節點的內存塊長度永遠大於左子樹上節點的內存塊長度. 但實際上子樹的根節點沒有特定的排序關系(決定子樹長度分界線是取決於整個樹的關系). 尋找樹上最小的空閑內存塊可以通過一直索引左子樹找到. 與通常的二叉樹一直尋找左子樹直到遇到空節點的方式不同我們會在左子樹為空時查找右子樹直到一個節點的左右子樹均為空, 最小的內存塊就在這條路徑上(這段話稍稍有些難以理解, 后文會結合代碼分析該二叉樹的實現). 最壞情況下執行增加/查找/移除節點的步驟與bin的個數相關, 在32bit下為6到21次, 64bit下最高到53次(原因下文再分析).
講完兩種最基本的內存塊管理單元, 接下來我們來看下dlmalloc核心數據結構malloc_state.
1 #define NSMALLBINS (32U) 2 #define NTREEBINS (32U) 3 #define SMALLBIN_SHIFT (3U) 4 #define SMALLBIN_WIDTH (SIZE_T_ONE << SMALLBIN_SHIFT) 5 #define TREEBIN_SHIFT (8U) 6 #define MIN_LARGE_SIZE (SIZE_T_ONE << TREEBIN_SHIFT) 7 #define MAX_SMALL_SIZE (MIN_LARGE_SIZE - SIZE_T_ONE) 8 #define MAX_SMALL_REQUEST (MAX_SMALL_SIZE - CHUNK_ALIGN_MASK - CHUNK_OVERHEAD) 9 struct malloc_state { 10 binmap_t smallmap; 11 binmap_t treemap; 12 size_t dvsize; 13 size_t topsize; 14 char* least_addr; 15 mchunkptr dv; 16 mchunkptr top; 17 size_t trim_check; 18 size_t release_checks; 19 size_t magic; 20 mchunkptr smallbins[(NSMALLBINS+1)*2]; 21 tbinptr treebins[NTREEBINS]; 22 size_t footprint; 23 size_t max_footprint; 24 flag_t mflags; 25 #if USE_LOCKS 26 MLOCK_T mutex; //dlmalloc自帶鎖, 一般未使用 27 #endif 28 msegment seg; 29 void* extp; //暫未使用 30 size_t exts; 31 }; 32 typedef struct malloc_state* mstate;
觀察malloc_state結構可以發現它有四個內存塊指針, 分別是類型為mchunkptr的dv, top, smallbins與類型為tbinptr的treebins, 其中smallbins與treebins是兩個數組, 而dlmalloc的內存管理就是圍繞着這四個內存塊展開的. 再展開malloc_state結構之前我們先思考一個問題: 內存管理器是否應對所有內存申請請求一視同仁? 答案顯然是否定的, 對於小長度的內存申請往往更頻繁且長度更穩定(往往是某種數據結構, 長度一般固定), 而反之較大的內存申請更少見(如果有頻繁的申請肯定會采取預留內存或外部分配的方式). 因此我們的內存管理工具要能更快的響應小塊內存申請, 同時兼顧其它長度的內存申請, 即既要考慮速度又要兼顧廣度, 讓我們看看dlmalloc是如何平衡兩者的. dlmalloc設計了smallbins與treebins數組來完成這項工作. smallbins是一個包含32個雙向鏈表的數組(實際長度是66個指針, 這個問題后面再討論), 每個雙向鏈表指向一種長度的malloc_chunk. 其長度覆蓋8字節到256字節(長度步進為8), 即第一個雙向鏈表指向長度為8的空閑內存塊, 第二個雙向鏈表指向長度為16的空閑內存塊, 依次類推. 位圖smallmap用來標記對應長度的空閑內存塊是否存在, 舉例第一位置零即不存在長度為8的空閑內存塊, 反之亦然. 在引入位圖與對應的雙向鏈表數組后就能保證以O(1)的速度索引所有小於256字節的空閑內存塊. 關於smallbins還有一個值得一提的小技巧, 那就是smallbins的數組長度是66, 這是為什么呢? 假設我們使用32個malloc_chunk的數組我們需要128個size_t大小的空間, 然而我們只使用了其中的一半空間(雙向鏈表), 因此dlmalloc將每個雙向鏈表的前兩個成員用作前一個malloc_chunk的雙向鏈表, 由此節省了一半的空間, 但是對於第一個成員它之前沒有空間了, 於是還需要額外增加2個size_t的空間. 那有人問我不將smallbins看做malloc_chunk數組, 僅將其看做雙向鏈表頭數組, 不也節省了空間嗎? 是的, 但是在后面我們會看到將其看做malloc_chunk可以簡化很多代碼判斷(因為malloc_chunk中的fd/bk不是指針鏈表頭的指針, 而是指向malloc_chunk的指針).
對於大於256字節的內存塊僅僅考慮快速分配是不夠了, 還要考慮擴大管理區間來降低管理開銷. 為了能夠加速分配時索引最合適的空閑內存, 大塊內存分配也使用了位圖treemap, 位圖的每一位代表了一顆二叉樹, 每兩顆二叉樹管理的長度區間是按2的冪次增長的(從2^8到2^24, 最后一棵樹管理所有比以上區間大的空閑內存塊), 具體排布如下:
idx range count
0 [256,384) 128
1 [384,512) 128
2 [512,768) 256
3 [768,1024) 256
4 [1024,1536) 512
......
通過以上兩個指針數組即可將所有空閑內存塊管理起來, 然而為了提升內存分配的效率dlmalloc還增加了兩個內存塊指針dv與top. 當請求大小的空閑內存塊不存在時dlmalloc會選擇稍大的一塊內存並將其切分成兩部分: 一部分用於響應內存申請, 另一部分即dv(designated victim). dv作用是保存最近的切分的內存塊中剩余部分, dvsize即該內存塊的長度. 它被單獨保存在dv中, 直到再次出現給定長度內存塊不存在需要切分大塊內存時再將它掛入對應鏈表/二叉樹, 如果有任何新的內存申請都會首先測試dv是否滿足需求, 借此dlmalloc盡可能減少內存的碎片化. top則是指當前活動內存段中最頂部的內存塊, 其大小保存在topsize中. 該內存塊的真實大小是topsize加上TOP_FOOT_SIZE, 即除自身長度外還包括隔離用的尾部malloc_chunk及段數據記錄的空間. 談到內存段的概念我們順帶來看下malloc_segment.
1 struct malloc_segment { 2 char* base; //段基址 3 size_t size; //段長度 4 struct malloc_segment* next; //下一個段地址 5 flag_t sflags; //標記位 6 }; 7 #define is_mmapped_segment(S) ((S)->sflags & IS_MMAPPED_BIT) 8 #define is_extern_segment(S) ((S)->sflags & EXTERN_BIT) 9 typedef struct malloc_segment msegment; 10 typedef struct malloc_segment* msegmentptr;
每個堆空間可能包含多個非連續的內存段, 每個內存段的信息都記錄在malloc_segment中, 保存在最頂部的空間中. 通過mmap直接分配的大塊內存不保存在該表中, 它們是獨立創建與銷毀的, 因而不跟蹤它們. 段管理主要用於mmap分配的空間, 通過mmap調用返回的地址可能與已存在的段相鄰也可能不相鄰, 不像MORECORE通常連續擴展當前地址(其地址總是相鄰的, 因而更容易處理, dlmalloc更偏向於使用它申請內存). 除了最上層的內存段, 其它內存段記錄都保存在這個段的尾部, 內存段通過將段記錄壓入mstate.seg鏈表中來維護. 內存段標記位作用: 如果EXTERN_BIT置位則不分配/釋放/合並該內存段(當前僅通過create_mspace_with_base初始化內存段時使用), 如果IS_MMAPPED_BIT置位則說明該段可以與其它mmap的內存段合並且需要通過munmap釋放, 如果沒有比特位置位則說明該內存段通過MORECORE申請且需要使用對應接口釋放.
到此dlmalloc的基本數據結構已經講解完了, 在進入接口分析之前我們最后看一個保存全局變量的結構.
1 struct malloc_params { 2 size_t magic; 3 size_t page_size; 4 size_t granularity; 5 size_t mmap_threshold; 6 size_t trim_threshold; 7 flag_t default_mflags; 8 }; 9 static struct malloc_params mparams;
mparams是靜態全局變量, 通過init_mparams()初始化. OK, 讓我們來看下dlmalloc的實現吧.
先復述下源碼對分配算法的注釋. 對於小於256字節的內存請求, 首先從smallbins中查找是否存在對應大小的空閑內存塊, 如存在則直接分配. 否則查詢dv是否滿足長度需求, 如滿足需求則使用dv內存塊. 否則從smallbins中尋找一個滿足大小要求的內存塊將其切分, 剩余部分存儲在dv中. 如仍未成功分配則嘗試使用top. 如top也不滿足要求嘗試從系統獲取內存. 對於更大的內存請求, 首先從treebins中查找滿足長度需求的最小內存塊與dv比較, 兩者中較合適的用於分配. 如兩者均不滿足則嘗試使用top進行分配. 如top不滿足需求且請求長度大小大於mmap門檻則嘗試直接映射內存. 否則向系統申請更多內存.
1 void* dlmalloc(size_t bytes) { 2 //gm為全局malloc_state結構, PREACTION用於定義USE_LOCKS時判斷是否加鎖 3 if (!PREACTION(gm)) { 4 void* mem; 5 size_t nb; 6 if (bytes <= MAX_SMALL_REQUEST) { 7 bindex_t idx; 8 binmap_t smallbits; 9 //實際申請長度為申請長度加管理前綴后按8字節對齊, 注意最小長度保護是sizeof(malloc_chunk) 10 nb = (bytes < MIN_REQUEST)? MIN_CHUNK_SIZE : pad_request(bytes); 11 idx = small_index(nb); 12 smallbits = gm->smallmap >> idx; 13 /** 14 * 查詢smallbins對應位是否有空閑內存塊, 注意此處與0x3的含義 15 * 如果下一個內存塊被切分為兩塊, 其剩余的8字節無法獨立作為一塊內存塊存在 16 * 所以不如在此處直接判斷是否有需求, 有則直接分配 17 * 18 **/ 19 if ((smallbits & 0x3U) != 0) { 20 mchunkptr b, p; 21 idx += ~smallbits & 1; 22 //smallbin_at宏展開是((sbinptr)((char*)&((M)->smallbins[(i)<<1]))) 23 b = smallbin_at(gm, idx); 24 p = b->fd; 25 assert(chunksize(p) == small_index2size(idx)); 26 //將p從鏈表中取出, 如果p為最后一個節點還要清除位圖對應位 27 unlink_first_small_chunk(gm, b, p, idx); 28 //置位cinuse與pinuse, 原因見前文敘述, 注意還要置位后一塊內存塊的pinuse 29 set_inuse_and_pinuse(gm, p, small_index2size(idx)); 30 //chunk2mem宏展開是((void*)((char*)(p)+TWO_SIZE_T_SIZES)) 31 mem = chunk2mem(p); 32 check_malloced_chunk(gm, mem, nb); 33 goto postaction; 34 } 35 //如果smallbins中沒有合適的選擇, 那么先判斷dv是否滿足需求 36 else if (nb > gm->dvsize) { 37 //如果dv滿足不了長度需求再判斷smallbins中是否有大於該長度的空閑內存塊 38 if (smallbits != 0) { 39 mchunkptr b, p, r; 40 size_t rsize; 41 bindex_t i; 42 /** 43 * left_bits宏展開是((x<<1) | -(x<<1)) 44 * least_bit宏展開是((x) & -(x)) 45 * same_or_left_bits宏展開是((x) | -(x)) 46 * 關於以上位元操作的詳細解釋見HACKER DELIGHT 47 * 48 **/ 49 binmap_t leftbits = (smallbits << idx) & left_bits(idx2bit(idx)); 50 binmap_t leastbit = least_bit(leftbits); 51 //二分查找位元位置, 此技巧的解釋也可見HACKER DELIGHT 52 compute_bit2idx(leastbit, i); 53 b = smallbin_at(gm, i); 54 p = b->fd; 55 assert(chunksize(p) == small_index2size(i)); 56 unlink_first_small_chunk(gm, b, p, i); 57 rsize = small_index2size(i) - nb; 58 //如果size_t大小不為4字節(64bit平台)且剩余大小又不足MIN_CHUNK_SIZE則不切分內存塊 59 if (SIZE_T_SIZE != 4 && rsize < MIN_CHUNK_SIZE) 60 set_inuse_and_pinuse(gm, p, small_index2size(i)); 61 else { 62 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 63 r = chunk_plus_offset(p, nb); 64 set_size_and_pinuse_of_free_chunk(r, rsize); 65 //此處先將old dv放回smallbin中, 再將切割出多余部分設為dv 66 replace_dv(gm, r, rsize); 67 } 68 mem = chunk2mem(p); 69 check_malloced_chunk(gm, mem, nb); 70 goto postaction; 71 } 72 //嘗試從treebin中分配, tmalloc_small()見后文分析 73 else if (gm->treemap != 0 && (mem = tmalloc_small(gm, nb)) != 0) { 74 check_malloced_chunk(gm, mem, nb); 75 goto postaction; 76 } 77 } 78 } 79 //過大的請求直接返回失敗 80 else if (bytes >= MAX_REQUEST) 81 nb = MAX_SIZE_T; 82 //對於大於256字節的請求走該分支處理 83 else { 84 nb = pad_request(bytes); 85 if (gm->treemap != 0 && (mem = tmalloc_large(gm, nb)) != 0) { 86 check_malloced_chunk(gm, mem, nb); 87 goto postaction; 88 } 89 } 90 //嘗試從dv中分配 91 if (nb <= gm->dvsize) { 92 size_t rsize = gm->dvsize - nb; 93 mchunkptr p = gm->dv; 94 if (rsize >= MIN_CHUNK_SIZE) { 95 mchunkptr r = gm->dv = chunk_plus_offset(p, nb); 96 gm->dvsize = rsize; 97 set_size_and_pinuse_of_free_chunk(r, rsize); 98 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 99 } 100 else { 101 size_t dvs = gm->dvsize; 102 gm->dvsize = 0; 103 gm->dv = 0; 104 set_inuse_and_pinuse(gm, p, dvs); 105 } 106 mem = chunk2mem(p); 107 check_malloced_chunk(gm, mem, nb); 108 goto postaction; 109 } 110 //最后嘗試從top中分配 111 else if (nb < gm->topsize) { 112 size_t rsize = gm->topsize -= nb; 113 mchunkptr p = gm->top; 114 mchunkptr r = gm->top = chunk_plus_offset(p, nb); 115 r->head = rsize | PINUSE_BIT; 116 set_size_and_pinuse_of_inuse_chunk(gm, p, nb); 117 mem = chunk2mem(p); 118 check_top_chunk(gm, gm->top); 119 check_malloced_chunk(gm, mem, nb); 120 goto postaction; 121 } 122 //以上都失敗求助sys_alloc(), 見后文分析 123 mem = sys_alloc(gm, nb); 124 postaction: 125 POSTACTION(gm); 126 return mem; 127 } 128 return 0; 129 }
dlmalloc()中主要是smallbins分配的實現, treebins分配見tmalloc_small()與tmalloc_large().
1 static void* tmalloc_small(mstate m, size_t nb) { 2 tchunkptr t, v; 3 size_t rsize; 4 bindex_t i; 5 binmap_t leastbit = least_bit(m->treemap); 6 compute_bit2idx(leastbit, i); 7 v = t = *treebin_at(m, i); 8 rsize = chunksize(t) - nb; 9 //查找長度最小的節點, 注意leftmost_child宏在左子樹為空時索引右子樹 10 while ((t = leftmost_child(t)) != 0) { 11 size_t trem = chunksize(t) - nb; 12 if (trem < rsize) { 13 rsize = trem; 14 v = t; 15 } 16 } 17 if (RTCHECK(ok_address(m, v))) { 18 mchunkptr r = chunk_plus_offset(v, nb); 19 assert(chunksize(v) == rsize + nb); 20 if (RTCHECK(ok_next(v, r))) { 21 unlink_large_chunk(m, v); 22 //剩余內存塊小於MIN_CHUNK_SIZE則一起分配, 否則存放在dv中, 與smallbins邏輯類似 23 if (rsize < MIN_CHUNK_SIZE) 24 set_inuse_and_pinuse(m, v, (rsize + nb)); 25 else { 26 set_size_and_pinuse_of_inuse_chunk(m, v, nb); 27 set_size_and_pinuse_of_free_chunk(r, rsize); 28 replace_dv(m, r, rsize); 29 } 30 return chunk2mem(v); 31 } 32 } 33 CORRUPTION_ERROR_ACTION(m); 34 return 0; 35 }
由於是從treebins中分配小塊內存需求, 所以tmalloc_small()直接查找最小的非空二叉樹上最小的空閑內存塊. tmalloc_small()的難點主要在對二叉樹的操作, 這里我們來分析下兩個相關的宏insert_large_chunk()/unlink_large_chunk()來加強對二叉樹的理解.
1 #define insert_large_chunk(M, X, S) {\ 2 tbinptr* H;\ 3 bindex_t I;\ 4 compute_tree_index(S, I);\ 5 H = treebin_at(M, I);\ 6 X->index = I;\ 7 X->child[0] = X->child[1] = 0;\ 8 if (!treemap_is_marked(M, I)) {\ 9 mark_treemap(M, I);\ 10 *H = X;\ 11 X->parent = (tchunkptr)H;\ 12 X->fd = X->bk = X;\ 13 }\ 14 else {\ 15 tchunkptr T = *H;\ 16 size_t K = S << leftshift_for_tree_index(I);\ 17 for (;;) {\ 18 if (chunksize(T) != S) {\ 19 tchunkptr* C = &(T->child[(K >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]);\ 20 K <<= 1;\ 21 if (*C != 0)\ 22 T = *C;\ 23 else if (RTCHECK(ok_address(M, C))) {\ 24 *C = X;\ 25 X->parent = T;\ 26 X->fd = X->bk = X;\ 27 break;\ 28 }\ 29 else {\ 30 CORRUPTION_ERROR_ACTION(M);\ 31 break;\ 32 }\ 33 }\ 34 else {\ 35 tchunkptr F = T->fd;\ 36 if (RTCHECK(ok_address(M, T) && ok_address(M, F))) {\ 37 T->fd = F->bk = X;\ 38 X->fd = F;\ 39 X->bk = T;\ 40 X->parent = 0;\ 41 break;\ 42 }\ 43 else {\ 44 CORRUPTION_ERROR_ACTION(M);\ 45 break;\ 46 }\ 47 }\ 48 }\ 49 }\ 50 }
insert_large_chunk()宏的作用是將長度為S的內存塊X插入treebins的管理中. 在這其中有兩種情況, 如樹中已存在相同長度內存塊則僅需將X加入雙向鏈表, 否則該內存塊需先加入二叉樹管理. 而后者又有兩種情況, 一是長度S所在的區間的樹不存在則X為該樹的根節點, 否則需要為X索引一個合適的葉子節點. 讓我們來看看insert_large_chunk()的實現: 首先根據長度S計算treebins的下標I, 初始化X的索引為I與左右子樹為空. 如treemap對應位置零則將X作為該樹的根節點. 否則需要索引X在樹中的位置, 索引的方式是根據長度S的高位來判斷是屬於左子樹還是右子樹. 注意此處leftshift_for_tree_index()宏作用: 對於給定下標I的樹其管理的長度是2^(7+I/2), 即長度的有效位在0到(7+I/2)(高位都是0), 所以左移(25-I/2)位, 高位即長度位. 通過循環判斷高位狀態選擇左右子樹, 直到該節點為空或子樹節點的長度與X相同. 如果節點為空即說明不存在該長度的內存塊, 即需要在當前位置插入一個新節點, 如果節點的長度與X相同則將X加入節點的雙向鏈表中管理.
1 #define unlink_large_chunk(M, X) {\ 2 tchunkptr XP = X->parent;\ 3 tchunkptr R;\ 4 if (X->bk != X) {\ 5 tchunkptr F = X->fd;\ 6 R = X->bk;\ 7 if (RTCHECK(ok_address(M, F))) {\ 8 F->bk = R;\ 9 R->fd = F;\ 10 }\ 11 else {\ 12 CORRUPTION_ERROR_ACTION(M);\ 13 }\ 14 }\ 15 else {\ 16 tchunkptr* RP;\ 17 if (((R = *(RP = &(X->child[1]))) != 0) ||\ 18 ((R = *(RP = &(X->child[0]))) != 0)) {\ 19 tchunkptr* CP;\ 20 while ((*(CP = &(R->child[1])) != 0) ||\ 21 (*(CP = &(R->child[0])) != 0)) {\ 22 R = *(RP = CP);\ 23 }\ 24 if (RTCHECK(ok_address(M, RP)))\ 25 *RP = 0;\ 26 else {\ 27 CORRUPTION_ERROR_ACTION(M);\ 28 }\ 29 }\ 30 }\ 31 if (XP != 0) {\ 32 tbinptr* H = treebin_at(M, X->index);\ 33 if (X == *H) {\ 34 if ((*H = R) == 0) \ 35 clear_treemap(M, X->index);\ 36 }\ 37 else if (RTCHECK(ok_address(M, XP))) {\ 38 if (XP->child[0] == X) \ 39 XP->child[0] = R;\ 40 else \ 41 XP->child[1] = R;\ 42 }\ 43 else\ 44 CORRUPTION_ERROR_ACTION(M);\ 45 if (R != 0) {\ 46 if (RTCHECK(ok_address(M, R))) {\ 47 tchunkptr C0, C1;\ 48 R->parent = XP;\ 49 if ((C0 = X->child[0]) != 0) {\ 50 if (RTCHECK(ok_address(M, C0))) {\ 51 R->child[0] = C0;\ 52 C0->parent = R;\ 53 }\ 54 else\ 55 CORRUPTION_ERROR_ACTION(M);\ 56 }\ 57 if ((C1 = X->child[1]) != 0) {\ 58 if (RTCHECK(ok_address(M, C1))) {\ 59 R->child[1] = C1;\ 60 C1->parent = R;\ 61 }\ 62 else\ 63 CORRUPTION_ERROR_ACTION(M);\ 64 }\ 65 }\ 66 else\ 67 CORRUPTION_ERROR_ACTION(M);\ 68 }\ 69 }\ 70 }
再來看下如何從樹中刪除一個節點, 同樣存在兩種情況, 與插入操作不同的是被刪除的節點同時是二叉樹上的節點也可能僅僅是雙向鏈表中的一個節點. 如果X僅存在雙向鏈表管理中, 僅僅修改鏈表管理即可. 如果存在多個長度S的內存塊則X需將自己在樹中位置信息復制給下一個鏈表的節點, 同時其父節點與左右子樹也需要對應修改. 如果長度為S的內存塊僅X一個, 則需要選擇X的右子樹(沒有則左子樹)替代原X的位置.
分析完基本的二叉樹操作后我們再來看看如何從treebins中分配大塊內存.
1 static void* tmalloc_large(mstate m, size_t nb) { 2 tchunkptr v = 0; 3 size_t rsize = -nb; 4 tchunkptr t; 5 bindex_t idx; 6 //根據給定長度返回所在區間對應的樹的下標 7 compute_tree_index(nb, idx); 8 if ((t = *treebin_at(m, idx)) != 0) { 9 size_t sizebits = nb << leftshift_for_tree_index(idx); 10 tchunkptr rst = 0; 11 for (;;) { 12 tchunkptr rt; 13 size_t trem = chunksize(t) - nb; 14 //遍歷子樹每次保存最合適的內存塊, 如存在長度一致的內存塊則中斷循環 15 if (trem < rsize) { 16 v = t; 17 if ((rsize = trem) == 0) 18 break; 19 } 20 rt = t->child[1]; 21 t = t->child[(sizebits >> (SIZE_T_BITSIZE-SIZE_T_ONE)) & 1]; 22 //保存右子樹節點, 當索引左子樹為空時返回到右子樹選擇該節點 23 if (rt != 0 && rt != t) 24 rst = rt; 25 if (t == 0) { 26 t = rst; 27 break; 28 } 29 sizebits <<= 1; 30 } 31 } 32 //最合適的樹上沒有去相鄰的樹上尋找, 此時必然尋找最小的內存塊 33 if (t == 0 && v == 0) { 34 binmap_t leftbits = left_bits(idx2bit(idx)) & m->treemap; 35 if (leftbits != 0) { 36 bindex_t i; 37 binmap_t leastbit = least_bit(leftbits); 38 compute_bit2idx(leastbit, i); 39 t = *treebin_at(m, i); 40 } 41 } 42 //接上面只要t非空就一直查找最小的內存塊 43 while (t != 0) { 44 size_t trem = chunksize(t) - nb; 45 if (trem < rsize) { 46 rsize = trem; 47 v = t; 48 } 49 t = leftmost_child(t); 50 } 51 //比較索引到的內存塊與dv選擇小的內存塊進行切分與分配, 盡量保留大塊內存 52 if (v != 0 && rsize < (size_t)(m->dvsize - nb)) { 53 if (RTCHECK(ok_address(m, v))) { 54 mchunkptr r = chunk_plus_offset(v, nb); 55 assert(chunksize(v) == rsize + nb); 56 if (RTCHECK(ok_next(v, r))) { 57 unlink_large_chunk(m, v); 58 if (rsize < MIN_CHUNK_SIZE) 59 set_inuse_and_pinuse(m, v, (rsize + nb)); 60 else { 61 set_size_and_pinuse_of_inuse_chunk(m, v, nb); 62 set_size_and_pinuse_of_free_chunk(r, rsize); 63 insert_chunk(m, r, rsize); 64 } 65 return chunk2mem(v); 66 } 67 } 68 CORRUPTION_ERROR_ACTION(m); 69 } 70 return 0; 71 }
經過前面的描述, tmalloc_large()也不難理解了, 此處稍微值得一提的是compute_tree_index()宏. 該宏有多個實現, 為了便於分析我們使用純軟件實現的算法.
1 #define compute_tree_index(S, I)\ 2 {\ 3 size_t X = S >> TREEBIN_SHIFT;\ 4 if (X == 0)\ 5 I = 0;\ 6 else if (X > 0xFFFF)\ 7 I = NTREEBINS-1;\ 8 else {\ 9 unsigned int Y = (unsigned int)X;\ 10 unsigned int N = ((Y - 0x100) >> 16) & 8;\ 11 unsigned int K = (((Y <<= N) - 0x1000) >> 16) & 4;\ 12 N += K;\ 13 N += K = (((Y <<= K) - 0x4000) >> 16) & 2;\ 14 K = 14 - N + ((Y <<= K) >> 15);\ 15 I = (K << 1) + ((S >> (K + (TREEBIN_SHIFT-1)) & 1));\ 16 }\ 17 }
算法核心是基於二分法快速獲取給定長度所在區間的下標. 前文提到過treebins的數組區間, 對於小於256字節使用序號最小的樹, 大於2^(16+8)的使用序號最大的樹. 而在兩者之間的長度則需要使用二分法快速定位所在區間的序號. 因符號擴展, 如果長度減去一個定值后為負數, 其右移后高位全置位, 所以N即用來計算前導零的個數, 而K為15減去N. 對於指數級的映射求出前導零后相減即可獲取下標, 但treebins是每兩個樹長度指數增長1, 所以實際位置為2*K且還要通過次高位判斷是屬於奇數樹區間還是偶數樹區間.
如果以上分配均失敗則需求助與系統.
1 static void* sys_alloc(mstate m, size_t nb) { 2 char* tbase = CMFAIL; 3 size_t tsize = 0; 4 flag_t mmap_flag = 0; 5 //初始化mparams, 第一次分配才會進入這里 6 init_mparams(); 7 //大塊內存直接mmap, 減少內核頁表地址占用 8 if (use_mmap(m) && nb >= mparams.mmap_threshold) { 9 void* mem = mmap_alloc(m, nb); 10 if (mem != 0) 11 return mem; 12 } 13 if (MORECORE_CONTIGUOUS && !use_noncontiguous(m)) { 14 char* br = CMFAIL; 15 //獲取當前段位置, 如果top為0即第一次分配, 否則查找top地址落在哪個段上 16 msegmentptr ss = (m->top == 0)? 0 : segment_holding(m, (char*)m->top); 17 size_t asize = 0; 18 ACQUIRE_MORECORE_LOCK(); 19 if (ss == 0) { 20 //第一次分配需先獲取堆的起始位置, sbrk(0)即獲取當前數據段地址 21 char* base = (char*)CALL_MORECORE(0); 22 if (base != CMFAIL) { 23 asize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE); 24 if (!is_page_aligned(base)) 25 asize += (page_align((size_t)base) - (size_t)base); 26 if (asize < HALF_MAX_SIZE_T && (br = (char*)(CALL_MORECORE(asize))) == base) { 27 tbase = base; 28 tsize = asize; 29 } 30 } 31 } 32 else { 33 asize = granularity_align(nb - m->topsize + TOP_FOOT_SIZE + SIZE_T_ONE); 34 if (asize < HALF_MAX_SIZE_T && (br = (char*)(CALL_MORECORE(asize))) == ss->base+ss->size) { 35 tbase = br; 36 tsize = asize; 37 } 38 } 39 if (tbase == CMFAIL) { 40 if (br != CMFAIL) { 41 if (asize < HALF_MAX_SIZE_T && asize < nb + TOP_FOOT_SIZE + SIZE_T_ONE) { 42 size_t esize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE - asize); 43 if (esize < HALF_MAX_SIZE_T) { 44 char* end = (char*)CALL_MORECORE(esize); 45 if (end != CMFAIL) 46 asize += esize; 47 else { 48 (void) CALL_MORECORE(-asize); 49 br = CMFAIL; 50 } 51 } 52 } 53 } 54 if (br != CMFAIL) { 55 tbase = br; 56 tsize = asize; 57 } 58 else 59 //使用MORECORE分配連續內存失敗, 之后不再嘗試 60 disable_contiguous(m); 61 } 62 RELEASE_MORECORE_LOCK(); 63 } 64 //嘗試MMAP 65 if (HAVE_MMAP && tbase == CMFAIL) { 66 size_t req = nb + TOP_FOOT_SIZE + SIZE_T_ONE; 67 size_t rsize = granularity_align(req); 68 if (rsize > nb) { 69 char* mp = (char*)(CALL_MMAP(rsize)); 70 if (mp != CMFAIL) { 71 tbase = mp; 72 tsize = rsize; 73 mmap_flag = IS_MMAPPED_BIT; 74 } 75 } 76 } 77 //仍然失敗嘗試非連續的MORECORE 78 if (HAVE_MORECORE && tbase == CMFAIL) { 79 size_t asize = granularity_align(nb + TOP_FOOT_SIZE + SIZE_T_ONE); 80 if (asize < HALF_MAX_SIZE_T) { 81 char* br = CMFAIL; 82 char* end = CMFAIL; 83 ACQUIRE_MORECORE_LOCK(); 84 br = (char*)(CALL_MORECORE(asize)); 85 end = (char*)(CALL_MORECORE(0)); 86 RELEASE_MORECORE_LOCK(); 87 if (br != CMFAIL && end != CMFAIL && br < end) { 88 size_t ssize = end - br; 89 if (ssize > nb + TOP_FOOT_SIZE) { 90 tbase = br; 91 tsize = ssize; 92 } 93 } 94 } 95 } 96 if (tbase != CMFAIL) { 97 if ((m->footprint += tsize) > m->max_footprint) 98 m->max_footprint = m->footprint; 99 if (!is_initialized(m)) { 100 m->seg.base = m->least_addr = tbase; 101 m->seg.size = tsize; 102 m->seg.sflags = mmap_flag; 103 m->magic = mparams.magic; 104 m->release_checks = MAX_RELEASE_CHECK_RATE; 105 init_bins(m); 106 #if !ONLY_MSPACES 107 if (is_global(m)) 108 init_top(m, (mchunkptr)tbase, tsize - TOP_FOOT_SIZE); 109 else 110 #endif 111 { 112 mchunkptr mn = next_chunk(mem2chunk(m)); 113 init_top(m, mn, (size_t)((tbase + tsize) - (char*)mn) -TOP_FOOT_SIZE); 114 } 115 } 116 else { 117 //嘗試合並內存段 118 msegmentptr sp = &m->seg; 119 while (sp != 0 && tbase != sp->base + sp->size) 120 sp = (NO_SEGMENT_TRAVERSAL) 0 : sp->next; 121 if (sp != 0 && !is_extern_segment(sp) && 122 (sp->sflags & IS_MMAPPED_BIT) == mmap_flag && 123 segment_holds(sp, m->top)) { 124 sp->size += tsize; 125 init_top(m, m->top, m->topsize + tsize); 126 } 127 else { 128 if (tbase < m->least_addr) 129 m->least_addr = tbase; 130 sp = &m->seg; 131 while (sp != 0 && sp->base != tbase + tsize) 132 sp = (NO_SEGMENT_TRAVERSAL) 0 : sp->next; 133 if (sp != 0 && !is_extern_segment(sp) && 134 (sp->sflags & IS_MMAPPED_BIT) == mmap_flag) { 135 char* oldbase = sp->base; 136 sp->base = tbase; 137 sp->size += tsize; 138 return prepend_alloc(m, tbase, oldbase, nb); 139 } 140 else 141 add_segment(m, tbase, tsize, mmap_flag); 142 } 143 } 144 if (nb < m->topsize) { 145 size_t rsize = m->topsize -= nb; 146 mchunkptr p = m->top; 147 mchunkptr r = m->top = chunk_plus_offset(p, nb); 148 r->head = rsize | PINUSE_BIT; 149 set_size_and_pinuse_of_inuse_chunk(m, p, nb); 150 check_top_chunk(m, m->top); 151 check_malloced_chunk(m, chunk2mem(p), nb); 152 return chunk2mem(p); 153 } 154 } 155 MALLOC_FAILURE_ACTION; 156 return 0; 157 }
從系統申請內存的策略是盡可能保持連續內存: 如果MORECORE能連續擴展則使用MORECORE, 否則嘗試使用MMAP(可能連續可能不連續), 最后再使用非連續地址的MORECORE. 在申請到內存后還要檢查內存段是否可合並. 而對於超過一個系統頁的內存申請策略又有所區別, 對這類內存申請直接使用MMAP方便其釋放. 注意通過MMAP申請內存時需要加上6個size_t長度, 原因在前文已有解釋, 該長度用來保存本塊內存的前綴(1)加上前一塊內存的腳標(1)加上后一塊內存的最小大小(4).
1 static void* mmap_alloc(mstate m, size_t nb) { 2 size_t mmsize = mmap_align(nb + SIX_SIZE_T_SIZES + CHUNK_ALIGN_MASK); 3 if (mmsize > nb) { 4 char* mm = (char*)(DIRECT_MMAP(mmsize)); 5 if (mm != CMFAIL) { 6 size_t offset = align_offset(chunk2mem(mm)); 7 size_t psize = mmsize - offset - MMAP_FOOT_PAD; 8 mchunkptr p = (mchunkptr)(mm + offset); 9 p->prev_foot = offset | IS_MMAPPED_BIT; 10 (p)->head = (psize|CINUSE_BIT); 11 mark_inuse_foot(m, p, psize); 12 chunk_plus_offset(p, psize)->head = FENCEPOST_HEAD; 13 chunk_plus_offset(p, psize+SIZE_T_SIZE)->head = 0; 14 if (mm < m->least_addr) 15 m->least_addr = mm; 16 if ((m->footprint += mmsize) > m->max_footprint) 17 m->max_footprint = m->footprint; 18 assert(is_aligned(chunk2mem(p))); 19 check_mmapped_chunk(m, p); 20 return chunk2mem(p); 21 } 22 } 23 return 0; 24 }
最后我們看下dlfree(), 其實現大部分已在前文描述. 釋放內存的邏輯是先判斷與當前內存塊前后相鄰的內存是否空閑(可合並). 如果可合並則將其從對應的管理結構中分出來(), 重新計算合並后大小並執行合並, 注意合並的優先級是top最高, dv其次, 最后是bin.
1 void dlfree(void* mem) { 2 if (mem != 0) { 3 mchunkptr p = mem2chunk(mem); 4 #if FOOTERS 5 //腳標檢查, 用於保護內存 6 mstate fm = get_mstate_for(p); 7 if (!ok_magic(fm)) { 8 USAGE_ERROR_ACTION(fm, p); 9 return; 10 } 11 #else 12 #define fm gm 13 #endif 14 if (!PREACTION(fm)) { 15 check_inuse_chunk(fm, p); 16 if (RTCHECK(ok_address(fm, p) && ok_cinuse(p))) { 17 size_t psize = chunksize(p); 18 mchunkptr next = chunk_plus_offset(p, psize); 19 //先判斷前一塊內存與當前內存塊的關系 20 if (!pinuse(p)) { 21 size_t prevsize = p->prev_foot; 22 //前一塊空閑且有mmap標記說明是直接mmap的 23 if ((prevsize & IS_MMAPPED_BIT) != 0) { 24 prevsize &= ~IS_MMAPPED_BIT; 25 psize += prevsize + MMAP_FOOT_PAD; 26 if (CALL_MUNMAP((char*)p - prevsize, psize) == 0) 27 fm->footprint -= psize; 28 goto postaction; 29 } 30 //否則就合並兩塊內存 31 else { 32 mchunkptr prev = chunk_minus_offset(p, prevsize); 33 psize += prevsize; 34 p = prev; 35 if (RTCHECK(ok_address(fm, prev))) { 36 /** 37 * 如果前一塊內存不是dv則必然存在於bin中 38 * 不可能是top, 因為top是最后一塊內存 39 * 如果是dv則將當前塊合並給dv 40 * 如果是bin中則僅將前一塊內存解除管理 41 * 合並后的內存屬於哪個bin在最后判斷 42 * 43 **/ 44 if (p != fm->dv) { 45 unlink_chunk(fm, p, prevsize); 46 } 47 else if ((next->head & INUSE_BITS) == INUSE_BITS) { 48 fm->dvsize = psize; 49 set_free_with_pinuse(p, psize, next); 50 goto postaction; 51 } 52 } 53 else 54 goto erroraction; 55 } 56 } 57 //再判斷當前內存塊與后一塊內存的關系 58 if (RTCHECK(ok_next(p, next) && ok_pinuse(next))) { 59 if (!cinuse(next)) { 60 /** 61 * 后一塊內存多了一種可能性, 可能為top或dv或在bin中 62 * 如果為top或dv則將當前塊與前一塊(如果有)合並給top 63 * 如果是bin中則僅將前一塊內存解除管理, 合並后的內存屬於哪個bin在最后判斷 64 * 65 **/ 66 if (next == fm->top) { 67 size_t tsize = fm->topsize += psize; 68 fm->top = p; 69 p->head = tsize | PINUSE_BIT; 70 if (p == fm->dv) { 71 fm->dv = 0; 72 fm->dvsize = 0; 73 } 74 if (should_trim(fm, tsize)) 75 sys_trim(fm, 0); 76 goto postaction; 77 } 78 else if (next == fm->dv) { 79 size_t dsize = fm->dvsize += psize; 80 fm->dv = p; 81 set_size_and_pinuse_of_free_chunk(p, dsize); 82 goto postaction; 83 } 84 else { 85 size_t nsize = chunksize(next); 86 psize += nsize; 87 unlink_chunk(fm, next, nsize); 88 set_size_and_pinuse_of_free_chunk(p, psize); 89 if (p == fm->dv) { 90 fm->dvsize = psize; 91 goto postaction; 92 } 93 } 94 } 95 else 96 set_free_with_pinuse(p, psize, next); 97 //最后判斷合並后的內存塊長度屬於哪個bin管理並插入到對應結構中 98 if (is_small(psize)) { 99 insert_small_chunk(fm, p, psize); 100 check_free_chunk(fm, p); 101 } 102 else { 103 tchunkptr tp = (tchunkptr)p; 104 insert_large_chunk(fm, tp, psize); 105 check_free_chunk(fm, p); 106 if (--fm->release_checks == 0) 107 release_unused_segments(fm); 108 } 109 goto postaction; 110 } 111 } 112 erroraction: 113 USAGE_ERROR_ACTION(fm, p); 114 postaction: 115 POSTACTION(fm); 116 } 117 } 118 #if !FOOTERS 119 #undef fm 120 #endif 121 }
至此dlmalloc的基本實現已分析完, 出於簡化代碼考慮未討論FOOTERS宏與MSPACES宏. 實際上這兩者的用處還是比較大的, 前者用於判斷內存操作是否越界, 后者是多線程ptmalloc實現的基礎, 本文就不展開討論了. 最后稍稍提及一下dlmalloc的多線程實現ptmalloc.
我們知道glibc內置函數都是以__libc_*命名的, 內存分配函數也不例外. 在ptmalloc3.c中會將這組接口重定義為public_*(比如public_mALLOc/public_fREe). 以public_mALLOc()為例, 它通過mspace_malloc實現內存分配. 每個mspace對應一個arena, public_mALLOc()會首先調用arena_get宏獲取arena(其思路是首先嘗試該線程最近一次成功獲取的arena, 如獲取失敗則循環鏈表嘗試獲取一個, 如果都失敗則直接初始化一個新的arena), 通過arena獲取對應的mspace. 這種方式較dlmalloc中直接給全局變量加鎖的方式明顯降低了線程並發申請內存的沖突. 限於篇幅關於ptmalloc更多的實現就請各位自己分析吧.