http://blog.jobbole.com/75656/
原型:void* malloc(size_t size);
要求:
- malloc分配的内存大小至少为size参数所指定的字节数
- malloc的返回值是一个指针,指向一段可用内存的起始地址
- 多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉
- malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)
- 实现malloc时应同时实现内存大小调整和内存释放函数(即realloc和free)
背景知识:
一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。
进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
int brk(void *addr);
void *sbrk(intptr_t increment);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。
一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。
另外需要注意的是,由于Linux是按页进行内存映射的,所以如果break被设置为没有按页大小对齐,则系统实际上会在最后映射一个完整的页,从而实际已映射的内存空间比break指向的地方要大一些。但是使用break之后的地址是很危险的(尽管也许break之后确实有一小块可用内存地址)。
实现:
数据结构:
将堆内存空间以块(Block)的形式组织起来,每个块由meta区和数据区组成,meta区记录数据块的元信息(数据区大小、空闲标志位、指针等等),数据区是真实分配的内存区域,并且数据区的第一个字节地址即为malloc返回的地址。
typedef struct s_block *t_block; struct s_block { size_t size; /* 数据区大小 */ t_block next; /* 指向下个块的指针 */ int free; /* 是否是空闲块 */ int padding; /* 填充4字节,保证meta块长度为8的倍数 */ char data[1]; /* 这是一个虚拟字段,表示数据块的第一个字节,长度不应计入meta */ };
在block链中查找合适的block。一般来说有两种查找算法:
- First fit:从头开始,使用第一个数据区大小大于要求size的块所谓此次分配的块
- Best fit:从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块
两种方法各有千秋,best fit具有较高的内存使用率(payload较高),而first fit具有更好的运行效率。
first fit算法:
t_block find_block(t_block *last, size_t size) { t_block b = first_block; while(b && !(b->free && b->size >= size)) { *last = b; b = b->next; } return b; }
find_block从frist_block开始,查找第一个符合要求的block并返回block起始地址,如果找不到这返回NULL。这里在遍历时会更新一个叫last的指针,这个指针始终指向当前遍历的block。这是为了如果找不到合适的block而开辟新block使用的,具体会在接下来的一节用到。
开辟新的block
如果现有block都不能满足size的要求,则需要在链表最后开辟一个新的block。这里关键是如何只使用sbrk创建一个struct:
#define BLOCK_SIZE 24 /* 由于存在虚拟的data字段,sizeof不能正确计算meta长度,这里手工设置 */ t_block extend_heap(t_block last, size_t s) { t_block b; b = sbrk(0); if(sbrk(BLOCK_SIZE + s) == (void *)-1) return NULL; b->size = s; b->next = NULL; if(last) last->next = b; b->free = 0; return b; }
分裂block
First fit有一个比较致命的缺点,就是可能会让很小的size占据很大的一块block,此时,为了提高payload,应该在剩余数据区足够大的情况下,将其分裂为一个新的block。
void split_block(t_block b, size_t s) { t_block new; new = b->data + s; new->size = b->size - s - BLOCK_SIZE ; new->next = b->next; new->free = 1; b->size = s; b->next = new; }
malloc的实现
有了上面的代码,我们可以利用它们整合成一个简单但初步可用的malloc。注意首先我们要定义个block链表的头first_block,初始化为NULL;另外,我们需要剩余空间至少有BLOCK_SIZE + 8才执行分裂操作。
由于我们希望malloc分配的数据区是按8字节对齐,所以在size不为8的倍数时,我们需要将size调整为大于size的最小的8的倍数:
size_t align8(size_t s) { if(s & 0x7 == 0) return s; return ((s >> 3) + 1) << 3; } #define BLOCK_SIZE 24 void *first_block=NULL; /* other functions... */ void *malloc(size_t size) { t_block b, last; size_t s; /* 对齐地址 */ s = align8(size); if(first_block) { /* 查找合适的block */ last = first_block; b = find_block(&last, s); if(b) { /* 如果可以,则分裂 */ if ((b->size - s) >= ( BLOCK_SIZE + 8)) split_block(b, s); b->free = 0; } else { /* 没有合适的block,开辟一个新的 */ b = extend_heap(last, s); if(!b) return NULL; } } else { b = extend_heap(NULL, s); if(!b) return NULL; first_block = b; } return b->data; }
calloc:
- malloc一段内存
- 将数据区内容置为0
由于我们的数据区是按8字节对齐的,所以为了提高效率,我们可以每8字节一组置0,而不是一个一个字节设置。我们可以通过新建一个size_t指针,将内存区域强制看做size_t类型来实现。
void *calloc(size_t number, size_t size) { size_t *new; size_t s8, i; new = malloc(number * size); if(new) { s8 = align8(number * size) >> 3; for(i = 0; i < s8; i++) new[i] = 0; } return new; }