夥伴分配器的一個極簡實現

(感謝網友 @個人上鋪叫路遙 投稿)html

提起buddy system相信不少人不會陌生,它是一種經典的內存分配算法,大名鼎鼎的Linux底層的內存管理用的就是它。這裏不探討內核這麼複雜實現,而僅僅是將該算法抽象提取出來,同時給出一份及其簡潔的源碼實現,以便定製擴展。node

夥伴分配的實質就是一種特殊的「分離適配」,即將內存按2的冪進行劃分,至關於分離出若干個塊大小一致的空閒鏈 表,搜索該鏈表並給出同需求最佳匹配的大小。其優勢是快速搜索合併(O(logN)時間複雜度)以及低外部碎片(最佳適配best-fit);其缺點是內 部碎片,由於按2的冪劃分塊,若是碰上66單位大小,那麼必須劃分128單位大小的塊。但若需求自己就按2的冪分配,好比能夠先分配若干個內存池,在其基 礎上進一步細分就頗有吸引力了。git

能夠在維基百科上找到該算法的描述,大致如是:github

分配內存:算法

1.尋找大小合適的內存塊(大於等於所需大小而且最接近2的冪,好比須要27,實際分配32)數組

1.若是找到了,分配給應用程序。
2.若是沒找到,分出合適的內存塊。數據結構

1.對半分離出高於所需大小的空閒內存塊
2.若是分到最低限度,分配這個大小。
3.回溯到步驟1(尋找合適大小的塊)
4.重複該步驟直到一個合適的塊函數

釋放內存:性能

1.釋放該內存塊測試

1.尋找相鄰的塊,看其是否釋放了。
2.若是相鄰塊也釋放了,合併這兩個塊,重複上述步驟直到趕上未釋放的相鄰塊,或者達到最高上限(即全部內存都釋放了)。

上面這段文字對你來講可能看起來很費勁,沒事,咱們看個內存分配和釋放的示意圖你就知道了:

上圖中,首先咱們假設咱們一個內存塊有1024K,當咱們須要給A分配70K內存的時候,

  1. 咱們發現1024K的一半大於70K,而後咱們就把1024K的內存分紅兩半,一半512K。

  2. 而後咱們發現512K的一半仍然大於70K,因而咱們再把512K的內存再分紅兩半,一半是128K。

  3. 此時,咱們發現128K的一半小於70K,因而咱們就分配爲A分配128K的內存。

後面的,B,C,D都這樣,而釋放內存時,則會把相鄰的塊一步一步地合併起來(合併也必需按分裂的逆操做進行合併)。

咱們能夠看見,這樣的算法,用二叉樹這個數據結構來實現再合適不過了。

我在網上分別找到cloudwuwuwenbin寫的兩份開源實現和測試用例。實際上後一份是對前一份的精簡和優化,本文打算從後一份入手講解,由於這份實現真正體現了「極簡」二字,追求突破常規的,極致簡單的設計。網友對其評價甚高,甚至可用做教科書標準實現,看完以後回過頭來看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]合併後整個內存 就回歸到初始狀態。

仍是看一下源碼實現吧,首先是夥伴分配器的數據結構:

struct buddy2 {
  unsigned size;
  unsigned longest[1];
};

這裏的成員size代表管理內存的總單元數目(測試用例中是32),成員longest就是二叉樹的節點標記,代表所對應的內存塊的空閒單位,在下文中會分析這是整個算法中最精妙的設計。此處數組大小爲1代表這是能夠向後擴展的(注:在GCC環境下你能夠寫成longest[0],不佔用空間,這裏是出於可移植性考慮),咱們在分配器初始化的buddy2_new能夠看到這種用法。

struct buddy2* buddy2_new( int size ) {
  struct buddy2* self;
  unsigned node_size;
  int i;

  if (size < 1 || !IS_POWER_OF_2(size))
    return NULL;

  self = (struct buddy2*)ALLOC( 2 * size * sizeof(unsigned));
  self->size = size;
  node_size = size * 2;

  for (i = 0; i < 2 * size - 1; ++i) {
    if (IS_POWER_OF_2(i+1))
      node_size /= 2;
    self->longest[i] = node_size;
  }
  return self;
}

整個分配器的大小就是滿二叉樹節點數目,即所需管理內存單元數目的2倍。一個節點對應4個字節,longest記錄了節點所對應的的內存塊大小。

內存分配的alloc中,入參是分配器指針和須要分配的大小,返回值是內存塊索引。alloc函數首先將size調整到2的冪大小,並檢查是否超過最大限度。而後進行適配搜索,深度優先遍歷,當找到對應節點後,將其longest標記爲0,即分離適配的塊出來,並轉換爲內存塊索引offset返回,依據二叉樹排列序號,好比內存整體大小32,咱們找到節點下標[8],內存塊對應大小是4,則offset = (8+1)*4-32 = 4,那麼分配內存塊就從索引4開始日後4個單位。

int buddy2_alloc(struct buddy2* self, int size) {
  unsigned index = 0;
  unsigned node_size;
  unsigned offset = 0;

  if (self==NULL)
    return -1;

  if (size <= 0)
    size = 1;
  else if (!IS_POWER_OF_2(size))
    size = fixsize(size);

  if (self->longest[index] < size)
    return -1;

  for(node_size = self->size; node_size != size; node_size /= 2 ) {
    if (self->longest[LEFT_LEAF(index)] >= size)
      index = LEFT_LEAF(index);
    else
      index = RIGHT_LEAF(index);
  }

  self->longest[index] = 0;
  offset = (index + 1) * node_size - self->size;

  while (index) {
    index = PARENT(index);
    self->longest[index] =
      MAX(self->longest[LEFT_LEAF(index)], self->longest[RIGHT_LEAF(index)]);
  }

  return offset;
}

在函數返回以前須要回溯,由於小塊內存被佔用,大塊就不能分配了,好比下標[8]標記爲0分離出來,那麼其父節點下標[0]、[1]、[3]也須要相應大小的分離。將它們的longest進行折扣計算,取左右子樹較大值,下標[3]取4,下標[1]取8,下標[0]取16,代表其對應的最大空閒值。

在內存釋放的free接口,咱們只要傳入以前分配的內存地址索引,並確保它是有效值。以後就跟alloc作反向回溯,從最後的節點開始一直往上找到longest爲0的節點,即當初分配塊所適配的大小和位置。咱們將longest恢復到原來滿狀態的值。繼續向上回溯,檢查是否存在合併的塊,依據就是左右子樹longest的值相加是否等於原空閒塊滿狀態的大小,若是可以合併,就將父節點longest標記爲相加的和(多麼簡單!)。

void buddy2_free(struct buddy2* self, int offset) {
  unsigned node_size, index = 0;
  unsigned left_longest, right_longest;

  assert(self && offset >= 0 && offset < size);

  node_size = 1;
  index = offset + self->size - 1;

  for (; self->longest[index] ; index = PARENT(index)) {
    node_size *= 2;
    if (index == 0)
      return;
  }

  self->longest[index] = node_size;

  while (index) {
    index = PARENT(index);
    node_size *= 2;

    left_longest = self->longest[LEFT_LEAF(index)];
    right_longest = self->longest[RIGHT_LEAF(index)];

    if (left_longest + right_longest == node_size)
      self->longest[index] = node_size;
    else
      self->longest[index] = MAX(left_longest, right_longest);
  }
}

上面兩個成對alloc/free接口的時間複雜度都是O(logN),保證了程序運行性能。然而這段程序設計的獨特之處就在於使用加權來標記內存空閒狀態,而不是通常的有限狀態機,實際上longest既能夠表示權重又能夠表示狀態,狀態機就毫無必要了,所謂「少便是多」嘛!反 觀cloudwu的實現,將節點標記爲UNUSED/USED/SPLIT/FULL四個狀態機,反而會帶來額外的條件判斷和管理實現,並且還不如數值那 樣精確。從邏輯流程上看,wuwenbin的實現簡潔明瞭如同教科書通常,特別是左右子樹的走向,內存塊的分離合並,塊索引到節點下標的轉換都是一步到 位,不像cloudwu充斥了大量二叉樹的深度和長度的間接計算,讓代碼變得晦澀難讀,這些都是longest的功勞。一個「極簡」的設計每每在於你想不到的突破常規思惟的地方。

這份代碼惟一的缺陷就是longest的大小是4字節,內存消耗大。但cloudwu的博客上有人提議用logN來保存值,這樣就能實現uint8_t大小了,看,又是一個「極簡」的設計!

說實話,很難在網上找到比這更簡約更優雅的buddy system實現了——至少在Google上如此。

相關文章
相關標籤/搜索