提起buddy system相信很多人不會陌生,它是一種經典的記憶體分配演算法,大名鼎鼎的linux底層的記憶體管理用的就是它。這裡不**核心這麼複雜實現,而僅僅是將該演算法抽象提取出來,同時給出乙份及其簡潔的原始碼實現,以便定製擴充套件。
夥伴分配的實質就是一種特殊的「分離適配」,即將記憶體按2的冪進行劃分,相當於分離出若干個塊大小一致的空閒鍊錶,搜尋該鍊錶並給出同需求最佳匹配的大小。其優點是快速搜尋合併(o(logn)時間複雜度)以及低外部碎片(最佳適配best-fit);其缺點是內部碎片,因為按2的冪劃分塊,如果碰上66單位大小,那麼必須劃分128單位大小的塊。但若需求本身就按2的冪分配,比如可以先分配若干個記憶體池,在其基礎上進一步細分就很有吸引力了。
可以在維基百科上找到該演算法的描述,大體如是:
分配記憶體:
1.尋找大小合適的記憶體塊(大於等於所需大小並且最接近2的冪,比如需要27,實際分配32)
1.如果找到了,分配給應用程式。
2.如果沒找到,分出合適的記憶體塊。
1.對半分離出高於所需大小的空閒記憶體塊
2.如果分到最低限度,分配這個大小。
3.回溯到步驟1(尋找合適大小的塊)
4.重複該步驟直到乙個合適的塊
釋放記憶體:
1.釋放該記憶體塊
1.尋找相鄰的塊,看其是否釋放了。
2.如果相鄰塊也釋放了,合併這兩個塊,重複上述步驟直到遇上未釋放的相鄰塊,或者達到最高上限(即所有記憶體都釋放了)。
上面這段文字對你來說可能看起來很費勁,沒事,我們看個記憶體分配和釋放的示意圖你就知道了:
上圖中,首先我們假設我們乙個記憶體塊有1024k,當我們需要給a分配70k記憶體的時候,
我們發現1024k的一半大於70k,然後我們就把1024k的記憶體分成兩半,一半512k。然後我們發現512k的一半仍然大於70k,於是我們再把512k的記憶體再分成兩半,一半是128k。此時,我們發現128k的一半小於70k,於是我們就分配為a分配128k的記憶體。後面的,b,c,d都這樣,而釋放記憶體時,則會把相鄰的塊一步一步地合併起來(合併也必需按**的逆操作進行合併)。
我們可以看見,這樣的演算法,用二叉樹這個資料結構來實現再合適不過了。
我在網上分別找到cloudwu和wuwenbin寫的兩份開源實現和測試用例。實際上後乙份是對前乙份的精簡和優化,本文打算從後乙份入手講解,因為這份實現真正體現了「極簡」二字,追求突破常規的,極致簡單的設計。網友對其評價甚高,甚至可用作教科書標準實現,看完之後回過頭來看cloudwu的**就容易理解了。
分配器的整體思想是,通過乙個陣列形式的完全二叉樹來監控管理記憶體,二叉樹的節點用於標記相應記憶體塊的使用狀態,高層節點對應大的塊,低層節點對應小的塊,在分配和釋放中我們就通過這些節點的標記屬性來進行塊的分離合併。如圖所示,假設總大小為16單位的記憶體,我們就建立乙個深度為5的滿二叉樹,根節點從陣列下標[0]開始,監控大小16的塊;它的左右孩子節點下標[1~2],監控大小8的塊;第三層節點下標[3~6]監控大小4的塊……依此類推。
在分配階段,首先要搜尋大小適配的塊,假設第一次分配3,轉換成2的冪是4,我們先要對整個記憶體進行對半切割,從16切割到4需要兩步,那麼從下標[0]節點開始深度搜尋到下標[3]的節點並將其標記為已分配。第二次再分配3那麼就標記下標[4]的節點。第三次分配6,即大小為8,那麼搜尋下標[2]的節點,因為下標[1]所對應的塊被下標[3~4]占用了。
在釋放階段,我們依次釋放上述第一次和第二次分配的塊,即先釋放[3]再釋放[4],當釋放下標[4]節點後,我們發現之前釋放的[3]是相鄰的,於是我們立馬將這兩個節點進行合併,這樣一來下次分配大小8的時候,我們就可以搜尋到下標[1]適配了。若進一步釋放下標[2],同[1]合併後整個記憶體就回歸到初始狀態。
還是看一下原始碼實現吧,首先是夥伴分配器的資料結構: 1
2 3 4
struct
buddy2 ;
這裡的成員size表明管理記憶體的總單元數目(測試用例中是32),成員longest就是二叉樹的節點標記,表明所對應的記憶體塊的空閒單位,在下文中會分析這是整個演算法中最精妙的設計。此處陣列大小為1表明這是可以向後擴充套件的(注:在gcc環境下你可以寫成longest[0],不占用空間,這裡是出於可移植性考慮),我們在分配器初始化的buddy2_new可以看到這種用法。1 2
3 45 6
7 89 10
11 12
13 14
15 16
17 18
19struct
buddy2* buddy2_new(
int
size )
return
self;
}
整個分配器的大小就是滿二叉樹節點數目,即所需管理記憶體單元數目的2倍。乙個節點對應4個位元組,longest記錄了節點所對應的的記憶體塊大小。
記憶體分配的alloc中,入參是分配器指標和需要分配的大小,返回值是記憶體塊索引。alloc函式首先將size調整到2的冪大小,並檢查是否超過最大限度。然後進行適配搜尋,深度優先遍歷,當找到對應節點後,將其longest標記為0,即分離適配的塊出來,並轉換為記憶體塊索引offset返回,依據二叉樹排列序號,比如記憶體總體大小32,我們找到節點下標[8],記憶體塊對應大小是4,則offset = (8+1)*4-32 = 4,那麼分配記憶體塊就從索引4開始往後4個單位。1 2
3 45 6
7 89 10
11 12
13 14
15 16
17 18
19 20
21 22
23 24
25 26
27 28
29 30
31 32
33 34
int
buddy2_alloc(
struct
buddy2* self,
int
size)
self->longest[index] = 0;
offset = (index + 1) * node_size - self->size;
while
(index)
return
offset;
}
在函式返回之前需要回溯,因為小塊記憶體被占用,大塊就不能分配了,比如下標[8]標記為0分離出來,那麼其父節點下標[0]、[1]、[3]也需要相應大小的分離。將它們的longest進行折扣計算,取左右子樹較大值,下標[3]取4,下標[1]取8,下標[0]取16,表明其對應的最大空閒值。
在記憶體釋放的free介面,我們只要傳入之前分配的記憶體位址索引,並確保它是有效值。之後就跟alloc做反向回溯,從最後的節點開始一直往上找到longest為0的節點,即當初分配塊所適配的大小和位置。我們將longest恢復到原來滿狀態的值。繼續向上回溯,檢查是否存在合併的塊,依據就是左右子樹longest的值相加是否等於原空閒塊滿狀態的大小,如果能夠合併,就將父節點longest標記為相加的和(多麼簡單!)。1 2
3 45 6
7 89 10
11 12
13 14
15 16
17 18
19 20
21 22
23 24
25 26
27 28
29 30
void
buddy2_free(
struct
buddy2* self,
int
offset)
self->longest[index] = node_size;
while
(index)
}
上面兩個成對alloc/free介面的時間複雜度都是o(logn),保證了程式執行效能。然而這段程式設計的獨特之處就在於使用加權來標記記憶體空閒狀態,而不是一般的有限狀態機,實際上longest既可以表示權重又可以表示狀態,狀態機就毫無必要了,所謂「少即是多」嘛!反觀cloudwu的實現,將節點標記為unused/used/split/full四個狀態機,反而會帶來額外的條件判斷和管理實現,而且還不如數值那樣精確。從邏輯流程上看,wuwenbin的實現簡潔明瞭如同教科書一般,特別是左右子樹的走向,記憶體塊的分離合併,塊索引到節點下標的轉換都是一步到位,不像cloudwu充斥了大量二叉樹的深度和長度的間接計算,讓**變得晦澀難讀,這些都是longest的功勞。乙個「極簡」的設計往往在於你想不到的突破常規思維的地方。
這份**唯一的缺陷就是longest的大小是4位元組,記憶體消耗大。但cloudwu的部落格上有人提議用logn來儲存值,這樣就能實現uint8_t大小了,看,又是乙個「極簡」的設計!
說實話,很難在網上找到比這更簡約更優雅的buddy system實現了——至少在google上如此。
**:
如何設計乙個記憶體分配器
通常工程裡不推薦自己寫記憶體分配器,因為你費力寫乙個出來99 可能性沒有內建的好,且記憶體出bug難除錯 不過看書之餘,你也可以動手自己試試,當個玩具寫寫玩玩 1.實現教科書上的記憶體分配器 做乙個鍊錶指向空閒記憶體,分配就是取出一塊來,改寫鍊錶,返回,釋放就是放回到鍊錶裡面,並做好歸併。注意做好標...
STL中的記憶體分配器 (一)
題記 記憶體管理一直是c c 程式的 關於記憶體管理的話題,大致有兩類側重點,一類是記憶體的正確使用,例如c 中new和delete應該成對出現,用raii技巧管理記憶體資源,auto ptr等方面,很多c c 書籍中都使用技巧的介紹。另一類是記憶體管理的實現,如linux核心的slab分配器,st...
對於定長記憶體分配器的簡化實現
定長記憶體分配器 實現乙個 freelist,每個 freelist 用於分配固定大小的記憶體塊,比如用於分配 32位元組物件的固定記憶體分配器。每個固定記憶體分配器裡面有兩個鍊錶,openlist 用於儲存未分配的空閒物件,closelist用於儲存已分配的記憶體物件,那麼所謂的分配就是從 ope...