1. 程式人生 > >dlmalloc 2.8.6 原始碼詳解—[0]基礎知識

dlmalloc 2.8.6 原始碼詳解—[0]基礎知識

前述

Dlmalloc是一個著名的記憶體分配器,最早由Doug Lea在1980s年代編寫.由於早期C庫的內建分配器在某種程度上的缺陷,因此dlmalloc出現後立即獲得了廣泛應用,足見其出色的表現.儘管時至今日, dlmalloc中的技術在一些地方已然落後於時代,很多優秀的allocator如google的tcmalloc, freeBSD的jemalloc等在某些情況下效能可以達到dlmalloc的數十甚至上百倍.但前者的很多思想和基本演算法對後來者產生了深遠的影響.走進memory allocator的神祕世界, dlmalloc可說是最好的教材之一.
本篇文章試圖以初學者的角度全面闡述dlmalloc的設計思路和基本實現手法,以及記憶體分配相關的一些基礎知識.事實上,記憶體分配器的設計思路和我們日常生活中”批發-零售”的概念是一致的,很多看似複雜的演算法的基本原理都比較好理解.但是從實現的角度說,如果真的完全看懂dlmalloc的程式碼,則需要極大的耐心.因為Doug Lea本人在這份程式碼中使用了大量的技巧,對於初次接觸此類程式碼的人來說,很多寫法非常的古怪.然而當你耐下心細細體會作者的思路和意圖之後,會對之心悅誠服,感受到其匠心獨到之處.這對於開拓自身的程式設計視野也是具有莫大幫助的.
文件中使用的程式碼基於android4.4上的malloc版本.原始碼位於目錄,
bionic\libc\upstream-dlmalloc\Malloc.c
最新版本的dlmalloc原始碼請參考該網址,

ftp://gee.cs.oswego.edu/pub/misc/malloc.c

1 基礎知識

在正式開始介紹dlmalloc之前,首先需要了解一些與之相關的基礎知識,否則基本上很難看懂dlmalloc的程式碼和文件.

1.1 記憶體分配原理

最早接觸C/C++這類語言時, malloc/free或者new/delete都是常常被提及的.這些函式或運算子無論引數還是返回值都非常簡單.但具體來說,記憶體到底是如何從硬體上某個記憶體顆粒中的地址單元裡,一路返回到程式設計師的手裡呢?這絕非一個簡單的過程,事實上整個過程相當之複雜.另外,我們需要探討的記憶體分配器在其中又是扮演哪個角色呢?

這裡寫圖片描述

上面的圖大致上描述了記憶體分配的過程,注意這只是一個示意圖,它並不是很精確.首先我們需要知道的是allocator與普通程式一樣都工作在使用者態,並且其本身也是作為system heap的一部分。當用戶通過malloc之類的庫函式提交分配請求後,將首先由allocator查詢,如果在其內部保留的空間中找到滿足需求的記憶體塊,就將之返回給使用者(一般都是相對較小的塊).如果找不到合適的塊,則進一步向system發起請求,將劃分system heap上的一部分給allocator,再由後者劃撥給使用者.另一方面,在劃撥heap空間後,這部分新生成的空間其實還無法直接使用,系統會在合適的時機,通過名為MMU的硬體將實際實體記憶體上的某些區域(以page為單位),對映到需要使用的線性地址上.這樣就完成了一塊記憶體從申請到使用的全過程.

從這些描述上, 你可能會感受到一點, allocator和system之間的關係就如批發和零售的關係一樣. System作為批發商,手中握有大量的待批商品——線性地址.而allocator作為零售商,會不定期的從批發商手裡提貨,將這些一次性批來的地址再進行二次管理.當用戶程式作為顧客,向零售商購買商品時, allocator的整套演算法都是為了保證儘可能的將貨都賣出去而不砸在手裡,也就是儘量避免在尚有存貨的情況下向批發商購進新的貨品.同時,它還要保證能在最快的時間將這些貨賣出去. Allocator的初衷就是幫助使用者程式又快又好的管理system heap中的記憶體.

1.2 Linux程序地址空間

說到system heap,需要了解一下程序的線性地址空間佈局.如圖所示,

這裡寫圖片描述

關於此類內容, 介紹計算機體系架構的書中會有詳述, 此章節做一個簡單的介紹.當應用程式的二進位制映像被載入到一個32bit機器的程序地址空間後,會被定位成如下的區域(這裡與有些資料上介紹的略有不同,基本上該圖會更加準確一些).從低地址到高地址分別為,最開始的一段區域為保留區域, 這裡並不存放有效的程式碼和資料.

之後從某一個地址開始, 為程式的入口點(C語言在該地方會有一句跳轉,進入__cinit()).往上為程式碼段,存放編譯好的二進位制映像,基本上是一些指令,常量和字串.程式碼段之上是資料段, 存放所有全域性和靜態變數.

資料段再往上是bss段,存放所有未初始化的全域性和靜態變數.在程式一開始這段區域是一大片0.資料段和bss段也被合稱為靜態區.

資料段之上也就是我們平時常稱作heap的堆區.注意heap是從低到高向上生長的,另外在heap一開始會有一段隨機長度的offset區,加入這個主要是為了防止惡意程式碼的溢位型攻擊.因為只要保證堆的基址每次都是隨機的,就無法通過將惡意跳轉指令插入到某個固定地址而導致因緩衝區溢位造成系統控制權丟失.

再往上是一大片未開墾的荒蕪之地,雖然圖上畫的很小,但實際卻很大.在通過系統呼叫申請之前,這片地址是既不可寫也不可讀的,否則會導致segmentation fault.

在中間的空洞之上是另一片比較大的區域,為mmap區.這片區域主要是儲存檔案對映,包括程式中使用的動態連結庫so的對映,以及匿名檔案對映,或者程式中共享資料用的手動對映.這部分割槽域是從高地址向低地址擴充套件.同樣,在mmap區之上,也存在一片用於防止溢位的隨機offset區.

接著向上, 就是被稱作stack的棧區.圖中有些誤導的是棧區其實本來是無法擴充套件的,最大容量是一早在核心中設定好的.這裡標示的stack向低地址grows down指的是棧頂sp指標的生長方向.因此一旦sp超過這段區域的設定下限,就意味著爆棧了.同樣stack之上也存在防止溢位的隨機offset區.

前面說的所有區域, 在32bit機器上加起來為3GB大小.這3GB被稱作程序的使用者空間,在不同程序之間是不可見的(fork的父子程序某些值在寫入前雖然是一樣的,但寫入後也是不一樣的).之所以可以做到這一點在於每個程序控制塊中儲存了各自的頁表目錄和頁表,這樣通過MMU查詢到的物理頁面其實根本是兩個地址.具體資訊請查閱linux下mm的相關部分.

最後, 最高地址的1GB空間為系統所有的核心空間,所有核心程式碼包括核心驅動都執行在這部分中.與使用者空間不同的是,核心空間的對映是固定的,因此不同程序雖然有自己的使用者空間,卻有著相同的核心空間.顯然這部分地址也是我們使用者無法直接讀寫的(可以通過驅動將核心空間的地址和使用者空間對映在一起以實現訪問).

通過上面的介紹, 可以看到應用程式的記憶體分配事實上是通過在中間空洞兩邊的heap區和mmap區相對生長來完成的.因此我們常說的動態記憶體在堆區分配其實是不準確的.

1.3 術語

介紹dlmalloc之前,還需要了解一些相關術語及其含義,這些到後面就不再做過多解釋了.

  • Payload: 有效負載.指的是實際交給應用程式使用的記憶體大小.

  • Overhead: 負載,開銷.本意是為了滿足分配需求所消耗的記憶體量,實際在程式碼註釋中多指除了payload之外的額外開銷(有些書中也稱之為cookie).

  • Chunk: 區塊.是記憶體分配的基本單位,類似物質世界中的原子不可再分. dlmalloc對記憶體的管理基本上都是以chunk為單位.一個典型的chunk是由使用者程式使用的部分(payload)以及額外的標記資訊(overhead)組成.

  • Bin: 分箱.用來管理相同或同一區間大小的chunk.在dlmalloc中分為sbin和tbin兩種.

  • Mspace: 分配空間.說白了就是dlmalloc中記憶體池的叫法.在dlmalloc中可以管理多個mspace.如果不顯式宣告,將會使用一個全域性的匿名空間,或者使用者可以自行劃分空間交給dlmalloc管理.

  • Segment: 區段.一般情況下,記憶體分配都是在一片連續區間內開採(exploit).但也會遇到不連續的情況,這就需要分成若干個區段記錄.多個區段可以同屬一個mspace.

  • Fenceposts: 柵欄.大多數分配器中, fencepost起到非連續記憶體間的隔離作用.一般這種隔離被用做安全檢查.分配器會在fenceposts所在位置寫上特殊標記,一旦非連續記憶體間發生寫入溢位(overwrite)就可以通過異常的fenceposts值發出警告.

  • Bookkeeping: 記錄資訊.不同於每個chunk中的overhead,這裡指的是整個mspace控制塊的記錄資訊.往往這部分資訊都固定在mspace開始的一段空間,或者乾脆就放在地址空間的靜態區中.

  • Granularity: 粒度.這個粒度指的是從system heap上獲取記憶體的最小單位.一般來說該值至少為一個page size, 且必須以2為底.

  • Mmap: 本意是類unix系統的檔案對映呼叫.但在dlmalloc中表示的更寬泛,這裡指代可以在程序地址空間中開闢非連續記憶體空間的系統呼叫.

  • Morecore: 指可以在程序地址空間中開闢連續記憶體空間的系統呼叫.在類unix系統下morecore指的是sbrk呼叫.

  • Program break: 前面提到的sbrk()實際也是一個庫函式,真正起作用的是brk()系統呼叫.這個函式其實就是break的縮寫.所謂的break是一個代表程序heap區top-most位置的指標.當我們通過sbrk/brk向系統請求記憶體時,系統做的僅僅是移動break指標,記憶體就這樣被劃撥到heap中了.而當釋放記憶體時,就反方向移動該指標,記憶體就返回給系統.

  • Footprint: 從系統獲得的記憶體量.指的是當前dlmalloc從system heap獲取的記憶體總和.設立footprint一方面是為了方便統計,另一方面也可以限制dlmalloc從系統獲取的最大記憶體量.

  • Trimming: 裁剪.被dlmalloc管理的記憶體被free後,並不直接返還給系統,而是當積累到一定程度會通過一些演算法判斷system heap是否收縮(shrink),這個過程在dlmalloc中稱作auto-trimming.

1.4 位運算

Allocator在計算記憶體大小時為了加快處理速度,會使用大量位運算.基本上很多計算技巧已經成為通用化的公式了.熟悉這些計算方法會對理解dlmalloc的程式碼有巨大幫助.

這裡提一下Doug Lea推薦的一本書《Hacker’s Delight》.該書是一本詳細介紹各種位運算奇技淫巧的著作,讀之前需要做好被虐的準備.

1.4.1 地址對齊

我們知道, 在計算機硬體體系結構中,記憶體的讀寫必須是對齊到某一個數值上的,比如常見的32bit機器對齊在4位元組上.那麼諸如0x08000003這樣的地址就不能被直接訪問.

判斷一個地址或一段長度是否對齊到某個邊界上其實就是判斷該值能否被對齊邊界整除.我們以對齊到8上為例, 24/8 = 3餘0,說明是對齊的.而27/8 = 3餘3,就不是對齊的.從程式碼角度考慮,很容易得到這樣的結果,

這裡寫圖片描述

顯然, 取模運算的效能代價相對於allocator來說是巨大的,有必要考慮其他的方法.前面說到,所有硬體體系都需要記憶體讀寫對齊到某一數值上,但還有一點沒有提及,就是該數值必須是以2為底的指數.這一次,我們將所有的除數和被除數用二進位制來表示,
這裡寫圖片描述

我想你可能發現了一個有趣的規律, 8相當於23,而所有被8除的結果中,在二進位制下餘數都是被除數的最後3位.是否說如果n相當於2m次冪,則A除以n的餘數相當於二進位制下A的最後m位呢?答案是肯定的.換句話說,上面的判斷中,如果我們能知道n是2的幾次冪就可以通過取出A的最後m位來判斷是否對齊.

以2為底的指數在二進位制下還有一個有用的特性,

比如, 8的二進位制表示為1000b,而 8 – 1表示為111b.

16的二進位制表示為 10000b,而 16 – 1表示為1111b.

而32的二進位制表示為100000b, 32 – 1表示為11111b.

這樣我們又發現了第二個規律, 以2為底的m次冪的二進位制數減去1時,由於連續借位,最終出現除了MSB之外,低位全部被1填充的情況.因此我們有,
如果n為以2為底的m (m > 0, m = 1, 2, 3, …)次冪指數,當以n為對齊邊界時,稱n – 1為該對齊邊界的對齊掩碼(align mask).

瞭解上面的兩個特性後, 判斷某地址是否對齊到以2為底的指數時就變得簡單了,只要將原數值與對齊掩碼作與,判斷結果是否等於0即可,

這裡寫圖片描述

1.4.2 上對齊與下對齊

那麼對於一個非對齊的數值進行處理實際上存在兩種方式,上對齊(align up)和下對齊(align_down).還是以8為對齊邊界來討論,

這裡寫圖片描述

圖中, 28如果採用下對齊的方式,對齊到24,則只需要簡單捨去其餘數即可.操作的方法有兩種,一種是先右移再左移的方式,
這裡寫圖片描述

或者, 直接使用對齊掩碼的方式,

這裡寫圖片描述

如果選擇上對齊, 則需要另外的方法,

這裡寫圖片描述

如上圖所示, 首先將28加上一個偏移,使其跳躍到下一個對齊區間裡去,然後再對其使用下對齊方式,就間接實現了在當前區間中上對齊.這個偏移值為對齊邊界減1.為什麼這裡會減1呢?這裡與對齊掩碼什麼的無關了,因為若被對齊數本身已經處於對齊狀態,再加上一個對齊長度並與對齊掩碼運算後就會留在下一個對齊區間中,這個結果顯然是不正確的了.因此得到的程式碼如下,

這裡寫圖片描述

這個巨集就是可以被作為公式的記憶體對齊巨集.

1.4.3 計算對齊偏移量

有時候不需要直接獲取對齊後的地址,而是計算出一個對齊偏移量,

這裡寫圖片描述

這個巨集看起來複雜, 其實就是前面的組合, 首先判斷是否已經對齊,如果是則偏移量為0.沒有對齊則用n – A & (n-1)獲取偏移量,再與上對齊掩碼保證位數正確.事實上,大多數機器上後面的與操作其實是多餘的.