本文使用隐式空闲链表实现简单的动态内存分配。
动态内存分配器维护一个大块区域,也就是堆,处理动态的内存分配请求。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。
实现动态内存分配要考虑以下问题:
(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; }
运行结果:
参考资料:《深入理解计算机系统》,机械工业出版社。