1. 程式人生 > 實用技巧 >夥伴系統的一種簡單實現

夥伴系統的一種簡單實現

什麼是夥伴系統

夥伴系統是一種將資源分配出去,再回收回來的一種方法,典型的使用場景是記憶體池。

夥伴系統的基本原理

當要實現記憶體管理時,通常會面對分配粒度的問題。要對記憶體進行分配,如果固定粒度的話,一定會造成內部碎片,而且根據放置演算法的不同,還表現出不同的特點。

夥伴分配器可以實現這樣的作用:將記憶體將2的冪進行劃分:

  1. 需要分配記憶體時如果沒有合適的記憶體塊,會對半切分記憶體塊直到分離出合適大小的記憶體塊為止,最後再將其返回。
  2. 需要釋放記憶體時,會尋找相鄰的塊,如果其已經釋放了,就將這倆合併,再遞迴這個過程,直到無法再合併為止

夥伴系統的實現

夥伴分配器的資料結構在邏輯上的表示就像是一個完全二叉樹。大概像這樣:

當然實際編碼過程中並不會使用一個struct TreeNode的形式去把二叉樹的各個節點用指標連起來,因為是這個樹一定是完全二叉樹,所以可以使用一個數組來表示樹的結構。這裡分析一個COOLSHELL網站上提出極簡實現。

首先資料結構長這樣:

struct buddy {
  unsigned size;
  unsigned longest[0];
};

在C中,這就是一個變長陣列的定義。這個實現中,預設要管理的記憶體是2的冪次。這裡的size是指buffer的長度,longest陣列的長度是2*size-1,因為這是滿二叉樹的結點數,所以整個buddy的記憶體佔用大小就是2*size-1,longest陣列每個元素都對應一個二叉樹的結點,元素的值代表對應記憶體塊的空閒容量

。據此,初始化的程式碼如下:

inline bool is_power_of_2(unsigned x) {
    return !(x & (x - 1));
}

struct buddy* buddy_new(int size) {
    struct buddy* self;
    unsigned node_size;
    int i;

    if (size < 1 || !is_power_of_2(size)) {
        return NULL;
    }

    self = (struct buddy*)malloc(2 * size * sizeof(unsigned));
    self->size = size;
    node_size = size * 2;

    // 遍歷每個二叉樹結點,為其賦值
    for (i = 0; i < 2 * size - 1; ++i) {
		// 注意:1也是2的冪
        if (is_power_of_2(i + 1)) {
            node_size /= 2;
        }

        // 第一層的結點的值是2*size,第二層是size,第三次是size/2,以此類推
        // 每個值代表的是空閒記憶體塊的數量
        self->longest[i] = node_size;
    }

    return self;
}

可以看到,整個管理其所佔的記憶體塊大小是需要管理記憶體大小的兩倍。

在完成了初始化後,下面實現從夥伴分配器中分配記憶體的過程。基本思路是:

  1. 首先需要將要分配的size向上調整為2的冪次,比如33只能調整到64,並檢測是否超過最大限度
  2. 進行深度優先搜尋,搜到一個最合適的塊,將longest的元素值標記為0
  3. 最後將這個記憶體塊的偏移返回
unsigned fixsize(unsigned size) {
  size |= size >> 1;
  size |= size >> 2;
  size |= size >> 4;
  size |= size >> 8;
  size |= size >> 16;
  return size + 1;
}

inline unsigned left_leaf(unsigned index) {
    return index * 2 + 1;
}

inline unsigned right_leaf(unsigned index) {
    return index * 2 + 2;
}

inline unsigned parent(unsigned index) {
    return (index + 1) / 2 - 1;
}

int buddy_alloc(struct buddy *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); // 向上調整到2的冪次
    }
    
    // 如果根節點下掛的size都不夠分配,就返回失敗
    if(self->longest[index] < size) {
        return -1;
    }
    
    // 由大到小搜尋最符合size的結點
    // 並在搜尋的過程中,更新index,優先使用左孩子
    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);
        }
    }
    
    // 找到對應的結點了,就將其管理的空閒記憶體塊數量標記為0
    self->longest[index] = 0;
    
    // 這裡的node_size是對應層的結點所管理的記憶體的大小,而index是結點的編
    // 根據這個演算法,offset恰好是分配記憶體的起始/索引,從offset往後數size個位元組的記憶體都是可用的
    offset = (index + 1) * node_size - self->size;
    
    // 因為更新了longest[index]的標記,所以需要更新它上層所有父節點的標記
    // 其中的原理是,如果小塊記憶體被佔用,那麼大塊記憶體就不滿足原來的可用狀態了
    while(index) {
        index = parent(index);
        self->longest[index] = std::max(self->longest[left_leaf(index)],
                                       self->longest[right_leaf(index)]);
    }
    
    return offset;
}

釋放的介面實現的基本思路是:

  1. 函式傳入之前buddy_alloc返回的offset,也就是地址索引,確保是有效值
  2. 和buddy_alloc的方向相反,做回溯,恢復結點的值
  3. 檢查需要合併的記憶體塊
void buddy_free(struct buddy *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;
    
    // 找到被佔用的那個記憶體塊,並更新node_size,即那層的結點所管理的記憶體塊的大小
    for(; self->longest[index] != 0; index = parent(index)) {
        node_size *= 2;
        
        // 如果根節點的都被佔用了,則直接返回
        if(index == 0) {
            return;
        }
    }
    
    // 歸還記憶體,將longest恢復到原來結點的值
    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] = std::max(left_longest, right_longest);
        }
    }
}

怎麼用?

說到底的是夥伴系統只是個輔助系統,buddy的longest陣列並不能當作記憶體池來用,而是要自己額外分配一段和buddy的size大小符合的記憶體與buddy搭配使用。

比如拿到了buddy_alloc返回的offset,就取實際記憶體池的[offset, offset + size]這段記憶體使用。