夥伴系統的一種簡單實現
阿新 • • 發佈:2020-10-12
什麼是夥伴系統
夥伴系統是一種將資源分配出去,再回收回來的一種方法,典型的使用場景是記憶體池。
夥伴系統的基本原理
當要實現記憶體管理時,通常會面對分配粒度的問題。要對記憶體進行分配,如果固定粒度的話,一定會造成內部碎片,而且根據放置演算法的不同,還表現出不同的特點。
夥伴分配器可以實現這樣的作用:將記憶體將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; }
可以看到,整個管理其所佔的記憶體塊大小是需要管理記憶體大小的兩倍。
在完成了初始化後,下面實現從夥伴分配器中分配記憶體的過程。基本思路是:
- 首先需要將要分配的size向上調整為2的冪次,比如33只能調整到64,並檢測是否超過最大限度
- 進行深度優先搜尋,搜到一個最合適的塊,將longest的元素值標記為0
- 最後將這個記憶體塊的偏移返回
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;
}
釋放的介面實現的基本思路是:
- 函式傳入之前buddy_alloc返回的offset,也就是地址索引,確保是有效值
- 和buddy_alloc的方向相反,做回溯,恢復結點的值
- 檢查需要合併的記憶體塊
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]這段記憶體使用。