Glibc 中malloc的實現

本文轉自:http://blog.csdn.net/phenics/article/details/777053

1 前言

C語言提供了動態內存管理功能, 在C語言中, 程序員可以使用 malloc() 和 free() 函數顯式的分配和釋放內存. 關於 malloc() 和free() 函數, C語言標準只是規定了它們需要實現的功能, 而沒有對實現方式有什麼限制, 這多少讓那些追根究底的人感到有些許迷茫, 比如對於 free() 函數, 它規定一旦一個內存區域被釋放掉, 那麼就不應該再對其進行任何引用, 任何對釋放區域的引用都會導致不可預知的後果 (unperdictable effects). 那麼, 到底是什麼樣的不可預知後果呢? 這完全取決於內存分配器(memory allocator)使用的算法. 這篇文章試圖對 Linux glibc 提供的 allocator 的工作方式進行一些描述, 並希望可以解答上述類似的問題. 雖然這裏的描述侷限於特定的平臺, 但一般的事實是, 相同功能的軟件基本上都會採用相似的技術. 這裏所描述的原理也許在別的環境下會仍然有效. 另外還要強調的一點是, 本文只是側重於一般原理的描述, 而不會過分糾纏於細節, 如果需要特定的細節知識, 請參考特定 allocator 的源代碼. 最後, 本文描述的硬件平臺是 Intel 80x86, 其中涉及的有些原理和數據可能是平臺相關的.

因爲只是草草看了 ptmalloc 的源代碼, 並做了一些實驗, 而沒有仔細分析代碼. 所以文章中的一些內容難免不實, 甚至爲虛妄. 實在是因爲水平有限, 並非存心妄自揣測, 來愚人耳目. 如果讀者發現其中有任何錯誤, 請來信告之, 並歡迎來信討論. 另外, 文章中涉及一些闕值, 比如內存分配的位置, 以及 max_fast 大小等等, 會因具體的實現而異, 若與所述有出入, 請自己判斷原因.

2 x86平臺Linux程序的內存分佈
Linux 程序載入內存後, loader 會把可執行文件中的各個段依次載入到從某一地址開始的空間中(載入地址取決於 link editor(ld), 在我的機器上是0x8048000, 即128M處). 如圖1所 示, 首先被載入的是 “.text” 段, 然後是 “.data” 段, 最後是 “.bss” 段. 這可以看作是程序的開始空間. 程序所能訪問的最後的地址是0xbfffffff, 也就是到3G地址處, 3G以上的1G空間是內核使用的, 應用程序不可以直接訪問. 應用程序的堆棧從最高地址處開始向下生長, “.bss”段與堆棧之間的空間是空閒的. 這個區域可以供用戶自由使用, 但是它在剛開始的時候並沒有映射到內存空間內, 是不可訪問的. 在向內核請求分配該空間之前, 對這個空間的訪問會導致一個 “segmentation fault”. 用戶程序可以直接使用系統調用來管理這塊空間, 但更多的時候都是程序都是使用C語言提供的 malloc() 和 free() 函數來動態的申請和釋放內存.
這裏寫圖片描述

圖 1: Linux程序內存分佈示意圖

3 Allocator

GNU Libc 的內存分配器( allocator ) — ptmalloc 起源於 Doug Lea 的 malloc (請參看[1]). ptmalloc 實現了 malloc() , free() 以及一組其它的函數. 以提供動態內存管理的支持. allocator 處在用戶程序和內核之間, 它響應用戶的分配請求, 向操作系統申請內存, 然後將其返回給用戶程序, 爲了保持高效的分配, allocator 一般都會預先分配一塊大於用戶請求的內存, 並通過某種算法管理這塊內存. 來滿足用戶的內存分配要求, 用戶 free 掉的內存也並不是立即就返回給操作系統, 相反, allocator 會管理這些被 free 掉的空閒空間, 以應對用戶以後的內存分配要求. 也就是說, allocator 不但要管理已分配的內存塊, 還需要管理空閒的內存塊, 當響應用戶分配要求時, allocator 會首先在空閒空間中尋找一塊合適的內存給用戶, 在空閒空間中找不到的情況下才分配一塊新的內存. 爲實現一個高效的 allocator, 需要考慮很多的因素. 比如, allocator 本身管理內存塊所佔用的內存空間必須很小, 分配算法必須要足夠的快. Jonathan Bartlett 給出了一個簡單的 allocator 實現[2], 事先看看或許會對理解本文有所幫助. 另外插一句, Jonathan Bartlett 的書 “Programming from Ground Up” 對想要了解 linux 彙編和工作方式的入門者是個不錯的選擇.

4 chuck的組織

不管內存是在哪裏被分配的, 用什麼方法分配, 用戶請求分配的空間在 ptmalloc 中都使用一個 chunk 來表示. 用戶調用 free() 函數釋放掉的內存也並不是立即就歸還給操作系統, 相反, 它們也會被表示爲一個 chunk, ptmalloc 使用特定的數據結構來管理這些空閒的 chuck.

4.1 chuck

ptmalloc 在給用戶分配的空間的前後加上了一些控制信息, 用這樣的方法來記錄分配的信息, 以便完成分配和釋放工作. 一個使用中的chuck( 使用中, 就是指還沒有被free掉 ) 在內存中的樣子如圖2所示.
這裏寫圖片描述

圖 2: 使用中的chuck
在圖中, chunk 指針指向一個 chunk 的開始, 一個chunk 中包含了用戶請求的內存區域和相關的控制信息. 圖中的 mem 指針纔是真正返回給用戶的內存指針. chunk 的第二個域的最低一位爲p, 它表示前一個塊是否在使用中, p爲0則表示前一個 chunk 爲空閒, 這時 chunk 的第一個域 prev_size 纔有效, prev_size 表示前一個 chunk 的 size, 程序可以使用這個值來找到前一個 chunk 的開始. 當p爲1時, 表示前一個 chunk 正在使用中, prev_size 無效, 程序也就不可以得到前一個 chunk 的大小. 而不能對前一個 chunk 進行任何操作. ptmalloc 分配的第一個塊總是將p設爲1, 以防止程序引用到不存在的區域.

空閒 chunk 在內存中的結構如圖3所示,
這裏寫圖片描述

圖 3: 空閒的thunk
當 chunk 空閒時, 原本是用戶數據區的地方存儲了兩個指針, 指針 fd 指向後一個空閒的 chunk, 而 bk 指向前一個空閒的 chunk, ptmalloc 通過這兩個指針將大小相近的 chunk 連成一個雙向鏈表. 而不同的 chunk 鏈表又是通過 bins 或者 fastbins 來組織的(bins 在第5.1節介紹, fastbins 在第5.2節介紹).

4.2 chunk中的空間複用

爲了使得 chunk 所佔用的空間最小, ptmalloc 使用了空間複用, 一個 chunk 或者正在被使用, 或者已經被 free 掉, 所以 chunk 的中的一些域可以在使用狀態和空閒狀態表示不同的意義, 來達到空間複用的效果. 空閒時, 一個 chunk 中至少要4個 size_t 大小的空間, 用來存儲 prev_size, size , fd 和 bk (見圖3所 示). 也就是16 bytes. chuck 的大小要 align 到8 bytes. 當一個 chunk 處於使用狀態時, 它的下一個 chunk 的 prev_size 域肯定是無效的. 所以實際上, 這個空間也可以被當前 chunk 使用. 這聽起來有點不可思議, 但確實是合理空間複用的例子. 故而實際上, 一個使用中的 chunk 的大小的計算公式應該是:

[xleftmargin=1cm] in_use_size = ( 用戶請求大小 + 8 - 4 ) align to 8 bytes 這裏加8是因爲需要存儲 prev_size 和 size, 但又因爲向下一個 chunk “借”了4個bytes, 所以要減去4. 最後, 因爲空閒的 chunk 和使用中的 chunk 使用的是同一塊空間. 所以肯定要取其中最大者作爲實際的分配空間. 即最終的分配空間 chunk_size = max(in_use_size, 16). 這就是當用戶請求內存分配時, ptmalloc 實際需要分配的內存大小, 在後面的介紹中. 如果不是特別指明的地方, 指的都是這個經過轉換的實際需要分配的內存大小, 而不是用戶請求的內存分配大小.

5 空閒 chunk 容器

5.1 Bins

用戶 free 掉的內存並不是都會馬上歸還給系統, 相反, ptmalloc 會統一管理 heap 中的空閒的 chunk (關於heap, 請參照第6節中圖5), 當用戶進行下一次分配請求時, ptmalloc 會首先試圖在 heap 中空閒的 chunk 中挑選一塊給用戶, 這樣就避免了頻繁的系統調用, 降低了內存分配的開銷. ptmalloc 將 heap 中相似大小的 chunk 用雙向鏈表鏈接起來, 這樣的一個鏈表被稱爲一個bin. ptmalloc 共維護了128個bin, 並使用一個數組來存儲這些 bin(如圖4).
這裏寫圖片描述

圖 4: bins 結構示意圖
數組中的前64個 bin 稱爲 “exact bins”, “exact bins” 中的 chunk 具有相同的大小. 兩個相鄰的 bin 中的 chunk 大小相差8 bytes. “exact bins”中的 chunk 按照最近使用順序進行排列, 最後釋放的 chunk 被鏈接到鏈表的頭部, 而 allocation 是從尾部開始, 這樣, 每一個 chunk 都有相同的機會被 ptmalloc 選中. 後面的 bin 被稱作 “ordered bins”. “ordered bins” 中的每一個 bin 分別包含了一個給定範圍內的 chunk, 其中的 chunk 按大小序排列. 相同大小的 chunk 同樣按照最近使用順序排列. ptmalloc 使用 “smallest-first, best-fit” 原則在空閒 “ordered bins” 中查找合適的 chunk.

當空閒的 chunk 被鏈接到bin中的時候, ptmalloc 會把表示該 chunk 是否處於使用中的標誌 p 設爲0(注意, 這個標誌實際上處在下一個 chunk 中), 同時 ptmalloc 還會檢查它前後的 chunk 是否也是空閒的, 如果是的話, ptmalloc 會首先把它們合併爲一個大的 chunk, 然後將合併後的 chunk 放到 bin 中. 要注意的是, 並不是所有的 chunk 被釋放後就立即被放到bin中. ptmalloc 爲了提高分配的速度, 會把一些小的的 chunk 先放到一個叫做 fastbin的容器內.

5.2 Fastbins

一般的情況是, 程序在運行時會經常需要分配和釋放一些較小的內存空間. 當 allocator 合併了相鄰的幾個小的 chunk 之後, 也許馬上就會有另一個小塊內存的請求, 這樣 allocator 又需要從大的空閒內存中分出一塊出來, 這樣無疑是比較低效的, 故而, ptmalloc 中在分配過程中引入了 fastbins, 不大於 max_fast (72 bytes) 的 chunk 被 free 後, 首先會被放到 fastbins 中, fastbins 中的 chunk 並不改變它的使用標誌p. 這樣也就無法將它們合併, 當需要給用戶分配的 chunk 小於或等於 max_fast 時, ptmalloc 首先會在 fastbins 中查找相應的空閒塊(具體的分配算法請參考第7節), 然後纔會去查找 bins 中的空間 chunk. 在某個特定的時候, ptmalloc 會遍歷 fastbins 中的 chunk, 將相鄰的空閒 chunk 進行合併, 並將合併後的 chunk 放到 bins 中去.

5.3 Unsorted Bins

如果被用戶釋放的 chunk 大於 max_fast, 則按上面的敘述它應該會被放到 bins中. 但實際上, ptmalloc 還引入了一個稱爲 “unsorted bins”的隊列. 這些大於 max_fast 的chunk 首先會被放到 “unsorted bins” 隊列中, 在進行 malloc 操作的時候, 如果在 fastbins 中沒有找到合適的 chunk, 則 ptmalloc 會先在 “unsorted bins”中查找合適的空閒 chunk, 然後才查找 bins. 如果 “unsorted bins” 不能滿足分配要求. malloc 便會將 “unsorted bins” 中的 chunk 放到 bins 中, 然後再在 bins 中繼續進行查找和分配過程. 從這個過程可以看出來, “unsorted bins”可以看做是 bins 的一個緩衝區, 增加它只是爲了加快分配的速度, 忽略它對我們理解 ptmalloc 沒有太大的影響, 在本文中, 這個過程就不被考慮了.

5.4 例外的 chunk

並不是所有的 chunk 都按照上面的方式來組織, 實際上, 有兩種例外情況.

top chunk
在前面一直提到, ptmalloc 會預先分配一塊較大的空閒內存(也就是所爲的 heap), 而通過管理這塊內存來響應用戶的需求, 因爲內存是按地址從低向高進行分配的, 在空閒內存的最高處, 必然存在着一塊空閒 chunk, 叫做 “top chunk”. 當 bins 和 fastbins 都不能滿足分配需要的時候, ptmalloc 會設法在 “top chunk” 中分出一塊內存給用戶, 如果 “top chunk” 本身不夠大, 則 ptmalloc 會適當的增加它的大小(也就增加了 heap 的大小). 以滿足分配的需要, 實際上, “top chunk” 在分配時總是在 ‘fastbins 和 bins 之後被考慮, 所以, 不論 “top chunk” 有多大, 它都不會被放到 fastbins 或者是 bins 中.
mmaped chunk
當需要分配的 chunk 足夠大, 而且 fastbins 和 bins 都不能滿足要求, 甚至 “top chunk” 本身也不能滿足分配需求時, ptmalloc 會使用 mmap 來直接使用內存映射來將頁映射到進程空間(具體的情況, 請參考第6節). 這樣分配的 chunk 在被 free 時將直接解除映射, 於是就將內存歸還給了系統, 再次對這樣的內存區的引用將導致一個 “segmentation fault” 錯誤. 這樣的 chunk 也不會包含在任何 bin 中.

6 sbrk & mmap

ptmalloc 使用兩種方法向內存索取內存空間: sbrk 和 mmap. 它們用於不同的場合.

6.1 sbrk

如圖5所示,
這裏寫圖片描述

圖 5: 使用 sbrk 和 mmap 分配內存示意圖
.bss 段之上的這塊分配給用戶程序的空間被稱爲 heap (堆). start_brk 指向 heap 的開始, 而 brk 指向 heap 的頂部. 可以使用系統調用 brk 和 sbrk 來增加標識 heap 頂部的 brk 值, 從而線性的增加分配給用戶的 heap 空間. 在使用malloc之前, brk 的值等於start_brk, 也就是說 heap 大小爲0. ptmalloc 在開始時, 若請求的空間小於 DEFAULT_MMAP_THRESHOLD (128K bytes)時, ptmalloc 會調用sbrk增加一塊大小爲 ( 128 KB + chunk_size ) align 4K 的空間作爲heap. 這就是前面所說的 ptmalloc 所維護的分配空間, 當用戶請求內存分配時, 首先會在這個區域內找一塊合適的 chunk 給用戶. 當用戶釋放了 heap 中的 chunk 時, ptmalloc 又會使用 fastbins 和 bins 來組織空閒 chunk. 以備用戶的下一次分配(具體的分配過程見第7節). 若需要分配的 chunk 大小小於 DEFAULT_MMAP_THRESHOLD, 而 heap 空間又不夠, 則此時 ptmalloc 會通過 sbrk 調用來增加 heap 值, 也就是增加 “top chunk”的大小, 每次 heap 增加的值都會 align 到4k bytes.

6.2 mmap

當用戶的請求超過 DEFAULT_MMAP_THRESHOLD , 並且使用 sbrk 分配失敗的時候, ptmalloc 會嘗試使用 mmap 直接映射一塊內存到進程內存空間(我機器上是在0x40159000地址處). 使用 mmap 直接映射的 chunk 在釋放時直接解除映射, 而不再屬於進程的內存空間. 任何對該內存的訪問都會產生段錯誤. 而在 heap 中分配的空間則可能會留在進程內存空間內, 還可以再次引用(當然是很危險的).

7 malloc()

ptmalloc 的響應用戶內存分配要求的具體步驟爲:

獲取分配區的鎖, ptmalloc 對 Doug Lea malloc 的主要擴展便是增加了線程支持. 爲了防止多個線程同時訪問同一個分配區, 在進行分配之前需要取得分配區域的鎖, 如果主分配區域的鎖不能得到, 那麼會 ptmalloc 會建立一個新的分配區域供當前線程使用.

將用戶的請求大小轉換爲實際需要分配的空間大小(見第4.2節的相關介紹).

判斷所需分配 chunk 的大小是否滿足 chunk_size <= max_fast (max_fast 默認爲 72 bytes) , 如果是的話, 則轉下一步, 否則跳到第5步.

首先嚐試在 fastbins 中摘取一個所需大小的 chunk 分配給用戶. 如果可以找到, 則分配結束. 否則轉到下一步.

判斷所需大小是否處在 “exact bins” 中, 即判斷 chunk_size /leq 512 bytes 是否成立(見圖4). 如果 chunk 大小處在 “exact bins”中, 則轉下一步, 否則轉到第6步.

根據所需分配的 chunk 的大小, 找到具體所在的 “exact bins”, 並從該 bin 的尾部摘取一塊恰好滿足大小的 chunk. 若成功, 則分配結束, 否則, 轉到下一步.

到了這一步, 說明需要分配的是一塊大的內存, 或者, “exact bins” 中找不到合適的 chunk. 於是, ptmalloc 首先會遍歷 fastbins 中的 chunk , 將相鄰的 chunk 進行合併, 並鏈接到 bins 中, 然後從 “sorted bins” 中按照 “smallest-first, best-fit” 原則, 找一塊合適的 chunk, 從中劃分一塊所需大小的chunk, 並將剩下的部分鏈接回到 bins 中. 若操作成功, 則分配結束, 否則轉到下一步.

如果搜索 fastbins 和 bins 都沒有找到合適的 chunk, 那麼就需要操作 top chunk 來進行分配了. 判斷 top chunk 大小是否滿足所需 chunk 的大小, 如果是, 則從 top chunk 中分出一塊來. 否則轉到下一步.

到了這一步, 說明 top chunk 也不能滿足分配要求, 所以, 於是就有了兩個選擇: 調用 sbrk, 增加 top chunk 大小; 或者使用 mmap 來直接分配. 在這裏, 需要依靠 chunk 的大小來決定到底使用哪種方法. 判斷所需分配的 chunk 大小是否大於等於 DEFAULT_MMAP_THRESHOLD (128KB), 如果是的話, 則轉下一步, 調用 mmap 分配, 否則跳到第11步, 使用 sbrk 來增加 top chunk 的大小.

使用 mmap 系統調用在大約 0x40159000 (大約爲1G) 地址處爲程序的內存空間映射一塊 chunk_size align 4kB 大小的空間. 然後將內存指針返回給用戶.

判斷是否爲第一次調用 malloc, 若是, 則需要進行一次初始化工作, 分配一塊大小爲 (chunk_size + 128K) align 4KB 大小的空間作爲初始的 heap. 若已經初始化過了, 則調用 sbrk 增加 heap 空間, 使之滿足分配需求, 並將內存指針返回給用戶.

總結一下: 根據用戶請求分配的內存的大小, ptmalloc 有可能會在兩個地方爲用戶分配內存空間. 在第一次分配內存時, brk 值等於 start_brk, 所以實際上 heap 大小爲0, top chunk 大小也是0. 這時, 如果不增加 heap 大小, 就不能滿足任何分配要求. 所以, 若用戶的請求小於 DEFAULT_MMAP_THRESHOLD, 則 ptmalloc 會初始化heap. 然後在 heap 中分配空間給用戶, 以後的分配就基於這個 heap 進行. 若第一次用戶的請求就大於DEFAULT_MMAP_THRESHOLD, 則 ptmalloc 直接使用 mmap 分配一塊給用戶, 而 heap 也就沒有被初始化, 直到用戶第一次請求小於 DEFAULT_MMAP_THRESHOLD 的內存分配. 第一次以後的分配就比較複雜了, 簡單說來, ptmalloc 首先會查找 fastbins, 如果不能找到匹配的 chunk, 則查找 “exact bins”. 若還是不行, 則查找 “sorted bins”. 在 fastbins 和 “exact bins” 中的查找都需要精確匹配, 而在sorted bins 中查找時, 則遵循 “smallest-first, best-fit” 的原則, 不需要精確匹配. 若以上方法都失敗了, 則 ptmalloc 會考慮使用 top chunk. 若top chunk 也不能滿足分配要求. 而且所需 chunk 大小大於 DEFAULT_MMAP_THRESHOLD , 則使用 mmap 進行分配. 否則增加 heap. 增大 top chunk. 以滿足分配要求.

8 free()

free() 函數接受一個指向分配區域的指針作爲參數, 釋放該指針所指向的 chunk. 而具體的釋放方法則看該 chunk 所處的位置和該 chunk 的大小. free()函數的工作步驟如下:

free() 函數同樣首先需要獲取分配區的鎖, 來保證線程安全.

判斷傳入的指針是否爲0, 如果爲0, 則什麼都不做, 直接return. 否則轉下一步:

判斷所需釋放的 chunk 是否爲 mmaped chunk, 如果是, 則直接釋放 mmaped chunk, 解除內存空間映射. 該空間不再有效. 釋放完成. 否則跳到下一步.

判斷 chunk 的大小和所處的位置, 若 chunk_size <= max_fast , 並且 chunk 並不位於 heap 的頂部, 也就是說並不與 top chunk 相鄰, 則轉到下一步, 否則跳到第6步. (因爲與 top chunk 相鄰的小 chunk 也和 top chunk 進行合併, 所以這裏不僅需要判斷大小, 還需要判斷相鄰情況.)

將 chunk 放到 fastbins 中, chunk 放入到 fastbins 中時, 並不設置該 chunk 使用位. 也不與相鄰的 chunk 進行合併. 只是放進去, 如此而已. 做實驗的結果還發現ptmalloc 放入 fastbins 中的 chunk 中的用戶數據去全置爲 0. 但是在源代碼中找不到相關的代碼. 這一步做完之後釋放便結束了, 程序從 free() 函數中返回..

判斷前一個 chunk 是否處在使用中, 如果前一個塊也是空閒塊, 則合併. 並轉下一步.

判斷當前釋放 chunk 的下一個塊是否爲 top chunk, 如果是, 則轉第9步, 否則轉下一步.

判斷下一個 chunk 是否處在使用中, 如果下一個 chunk 也是空閒的. 則合併, 並將合併後的 chunk 放到 bins 中. 注意, 這裏在合併的過程中, 要更新 chunk 的大小, 以反映合併後的 chunk 的大小. 並轉到第10步.

如果執行到這一步, 說明釋放了一個與 top chunk 相鄰的chunk. 則無論它有多大, 都將它與 top chunk 合併, 並更新 top chunk 的大小等信息. 轉下一步.

判斷合併後的 chunk 的大小是否大於 FASTBIN_CONSOLIDATION_THRESHOLD, 如果是的話, 則會觸發進行 fastbins 的合併操作, fastbins 中的 chunk 將被遍歷, 並於相鄰的空閒 chunk 進行合併, 合併後的 chunk 會被放到 bins 中. fastbins 將變爲空, 操作完成之後轉下一步.

判斷 top chunk 的大小是否大於 DEFAULT_TRIM_THERESHOLD. 如果是的話, 則會試圖歸還 top chunk 中的一部分給操作系統. 但是最先分配的128KB的空間是不會歸還. ptmalloc 會一直控制這部分內存. 用於響應用戶的分配請求. 做完這一步之後, 釋放結束, 從 free 函數退出.

參考文獻
[1] Doug Lea. A Memory Allocator. http://gee.cs.oswego.edu/dl/html/malloc.html.
[2] Jonathan Bartlett. 內存管理內幕—動態分配的選擇、折衷和實現. http://www-128.ibm.com/developerworks/cn/linux/l-memory/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章