本文使用隱式空閒鏈表實現簡單的動態內存分配。
動態內存分配器維護一個大塊區域,也就是堆,處理動態的內存分配請求。分配器將堆視爲一組不同大小的塊的集合來維護,每個塊要麼是已分配的,要麼是空閒的。
實現動態內存分配要考慮以下問題:
(1)空閒塊組織:我們如何記錄空閒塊?
(2)放置:我們如何選擇一個合適的空閒塊來放置一個新分配的塊?
(3)分割:在我們將一個新分配的塊放置到某個空閒塊之後,我們如何處理這個空閒塊中的剩餘部分?
(4)合併:我們如何處理一個剛剛被釋放的塊?
任何分配器都需要一些開銷,需要數據結構來記錄信息,區分塊邊界,區分已分配塊和空閒塊等。大多數實現方式都把信息放在塊本身內部。隱式空閒鏈表就是通過每個塊的頭部中存放的信息可以方便的定位到下一個塊的位置。頭部一般就是本塊的大小及使用情況(分配或空閒)。
本塊的起始地址加上本塊的大小就是下一個塊的起始地址。
本文使用的控制塊結構如下:
struct mem_block
{
int size; // 本塊的大小,包括控制結構
int is_free; // 使用情況,1爲空閒,0爲已分配
}
爲了內存對齊,這裏is_free也是用4字節的int存儲。其實控制信息根本不需要這麼多,此處爲了方便理解。
下面是一個塊的表示圖
返回給用戶的區域並不包含控制信息。
當接收到一個內存分配請求時,從頭開始遍歷堆,找到一個空閒的滿足大小要求的塊,若有剩餘,將剩餘部分變成一個新的空閒塊,更新相關塊的控制信息。調整起始位置,返回給用戶。
釋放內存時,僅需把使用情況標記爲空閒即可。
隱式空閒鏈表的優點是簡單。顯著的缺點是任何操作的開銷,例如放置分配的塊,要求空閒鏈表的搜索與堆中已分配塊和空閒塊的總數呈線性關係。
搜索可以滿足請求的空閒塊時,常見的策略有以下幾種:
(1)首次適應法(First Fit):選擇第一個滿足要求的空閒塊
(2)最佳適應法(Best Fit):選擇滿足要求的,且大小最小的空閒塊
(3)最壞適應法(Worst Fit):選擇最大的空閒塊
(4)循環首次適應法(Next Fit):從上次分配位置開始找到第一個滿足要求的空閒塊
這裏不對各種策略的優劣進行比較了。
當找不到滿足請求的空閒塊時,並不代表就此失敗了,如,我們申請50個字節大小的塊,沒有找到滿足要求的,但可能存在存在兩個相鄰的塊都是空閒,且每個塊的大小是30字節,這種情況我們應該能夠處理。即合併空閒塊問題。有兩種策略,一個是立即合併,另一個是推遲合併。本文實現的是推遲合併,立即合併需要同時知道前後兩個塊的信息,需要額外的一些數據結構,大同小異。
下面是實現代碼及測試代碼:
#include<stdio.h>
#include<malloc.h>
// 內存對齊,至少應該是mem_block的大小,而且應該是4的整數倍
#define ALIGNMENT 8
// 初始化堆的大小
#define HEAP_SIZE 10000
// 控制信息結構體
struct mem_block
{
int size; // 本塊的大小
int is_free; // 是否已分配
};
typedef struct mem_block mem_block;
// 堆的起始地址和結束地址
void *g_heap_start = 0;
void *g_heap_end = 0;
bool g_heap_inited = false;
// 初始化堆
void init_simple_malloc()
{
g_heap_inited = true;
g_heap_start = malloc(HEAP_SIZE);
if(g_heap_start == 0)
return;
mem_block* pos = (mem_block*)g_heap_start;
pos->size = HEAP_SIZE;
pos->is_free = 1;
g_heap_end = (void*)((char*)g_heap_start+HEAP_SIZE-1);
}
// 內部使用的分配內存函數
void *_simple_malloc(size_t size)
{
if(g_heap_start == 0)
return 0;
// 調整內存大小,滿足對齊要求
size = (size+ALIGNMENT-1) & (~(ALIGNMENT-1));
mem_block *pos = (mem_block*)g_heap_start;
while((void*)pos < g_heap_end)
{
// 最先適應法
if(pos->is_free && pos->size >= sizeof(mem_block)+size)
{
if(pos->is_free == sizeof(mem_block)+size)
pos->is_free = 0;
else
{
// 取出需要的大小,剩下的成爲堆中的一個新塊
mem_block *next_pos = (mem_block*)((char*)pos+sizeof(mem_block)+size);
next_pos->is_free = 1;
next_pos->size = pos->size-sizeof(mem_block)-size;
pos->is_free = 0;
pos->size = sizeof(mem_block)+size;
}
return (void*)((char*)pos+sizeof(mem_block));
}
else
pos = (mem_block*)((char*)pos+pos->size);
}
return 0;
}
// 內部使用的合併空閒塊函數
void _merge_free_blocks()
{
mem_block *pos = (mem_block*)g_heap_start;
while((void*)((char*)pos+pos->size) < g_heap_end)
{
mem_block *next_pos = (mem_block*)((char*)pos+pos->size);
// 若相鄰的兩個塊都是空閒,合二爲一
if(pos->is_free && next_pos->is_free)
pos->size = pos->size+next_pos->size;
else
pos = next_pos;
}
return;
}
// 外部使用的內存分配函數
void *simple_malloc(size_t size)
{
if(!g_heap_inited)
init_simple_malloc();
void * pos = _simple_malloc(size);
if(pos)
return pos;
// 若第一次分配內存失敗,則進行合併空閒塊,再次嘗試分配
_merge_free_blocks();
return _simple_malloc(size);
}
// 外部使用的內存釋放函數
void simple_free(void *p)
{
mem_block * pos = (mem_block*)((char*)p-sizeof(mem_block));
// 釋放僅需標記一下
pos->is_free = 1;
return;
}
// 測試使用的打印堆信息函數
void print_heap_info()
{
mem_block *pos = (mem_block*)g_heap_start;
puts("Heap info:");
while((void*)pos < g_heap_end)
{
// 輸出堆中所有控制塊的起始地址,大小,使用情況
printf("mem_block info: start_addr, %d; size, %d; is_free, %d\n", pos, pos->size, pos->is_free);
pos = (mem_block*)((char*)pos+pos->size);
}
putchar('\n');
return;
}
int main()
{
void *p1 = simple_malloc(3000);
// 狀態一
puts("State 1");
print_heap_info();
void *p2 = simple_malloc(5000);
// 狀態二
puts("State 2");
print_heap_info();
void *p3 = simple_malloc(1000);
// 狀態三
puts("State 3");
print_heap_info();
simple_free(p1);
simple_free(p2);
simple_free(p3);
// 狀態四
puts("State 4");
print_heap_info();
void *p4 = simple_malloc(8000);
// 狀態五
puts("State 5");
print_heap_info();
simple_free(p4);
// 狀態六
puts("State 6");
print_heap_info();
return 0;
}
運行結果:
參考資料:《深入理解計算機系統》,機械工業出版社。