前置知识
本lab大致的要求就是实现malloc、free、initial函数。initial函数就是要初始化堆。
做这个lab之前,需要对一下知识有一个比较清晰的认知。
- 组织:如何组织空闲块,也就是如何初始化堆。
- 放置:如何放置一个新分配的块。
- 分割:因分配的块放到空闲块之后,如何处理空闲块的剩余部分。
- 合并:如何处理一个刚刚被释放的块。
组织空闲块
有这样几种方法可以组织空闲块:采用隐式空闲链表、采用显示空闲链表
隐式空闲链表
块的格式:
规定块是双字(8字节)对齐的,也就是块的大小总是8的倍数,所以后三位总是为0,这空出来的三位就来表示状态信息。而剩余的29个高位用来存放块的大小。假设有一个分配的块,大小为24字节,那么其头部状态为
1
| 0x18 | 0x1 = 0x19 //1为状态为,表示已分配
|
对于一个大小为24字节,没有被分配的块,其头部为
头部的后面就是有效载荷。有效载荷后面是一片不使用的填充快,其大小是任意的。最后的填充快可以用来对付外部碎片或者满足对齐要求。
每一个块都如此,组织成一个连续的已分配块和空闲块的序列:
这种数据结构叫做隐式空闲链表,因为空闲块是通过头部中的大小字段隐含的连接着的。分配器可以通过遍历堆中的所有块从而间接地遍历空闲块集合。最后有一个大小为0而显示已分配的块是终止头部,用来标记结束块。
优点:
简单
缺点:
效率低。任何操作都需要堆空闲链表进行搜索,搜索时间与块的总数呈线性关系。
显示空闲链表
块的样式:

以后补充。。。
放置策略
当请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的块。分配器的搜索方式是由防止策略确定的,常见的策略有:首次适配、下一次适配、最佳适配。
首次适配
从又开始搜索空闲链表,选择第一个合适的空闲块。
下一次适配
和首次适配相似,只是起点不是链表的头部而是上一次查询结束的的地方。
最佳适配
最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。
分割策略
匹配到合适的空闲块之后,可以选择占用整个空闲块,也可以选择分割为两部分,一部分变为分配块,剩下的部分变成一个新的空闲块。
合并策略
合并空闲块时必要的,否则会产生大量的内存碎片。
立即合并
每次一个块被释放时就合并所有的相邻块。
推迟合并
等到某个稍晚的时候合并,例如知道分配请求失败,然后扫描整个堆,合并所有的空闲块。
获取额外的堆空间
思考这样一种情况,在合并了所有的相邻空闲块之后,还是找不到一块足够大的空闲块来防止请求块,应该怎么办?
遇到这种情况,分配器会通过调用sbrk函数,向内核请求额外的堆内存,分配器将额外的内存转为一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
尝试实现一个简单的分配器
分配器说明:
- 基于隐式空闲链表
- 使用立即边界合并
- 最大的块大小为4GB
分配器使用memlib.c包所提供的内存系统模型,模型的目的在于允许我们在不干涉已存在的系统层malloc包的情况下运行分配器。
mm_init函数将对于队来说的可用的虚拟内存模型化为一个大的、双字对齐的字节数组。在mm_heap和mm_brk之间的字节表示已分配的虚拟内存。分配器通过mm_sbrk函数来请求额外的堆内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static char *mem_heap; static char *mem_brk; static char *mem_max_addr;
void mem_init(void) { mem_heap=(char*)Malloc(MAX_HEAP); mem_brk=(char*)mem_heap; mem_max_addr=(char*)(mem_heap+MAX_HEAP); }
void *mem_sbrk(int incr) { char *old_brk=mem_brk; if((incr<0)||((mem_brk+incr)>mem_max_addr)) { errno=ENOMEM; fprintf(stderr,"ERROR: mem_sbrk failed.RAN out of memory...\n"); return (void*)-1; } mem_brk+=incr; return (void*)old_brk; }
|
书中提供了一组常数和宏来简化空闲链表操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #define WSIZE 4 #define DSIZE 8 #define CHUNKSIZE (1<<12)
#define MAX(x,y) ((x)>(y)?(x):(y))
#define PACK(size,alloc) ((size)|(alloc))
#define GET(p) (*(unsigned int*)(p)) #define PUT(p,val) (*(unsigned int*)(p) = (val))
#define GET_SIZE(p) (GET(p)&~0x7) #define GET_ALLOC(p) (GET(p)&0x1)
#define HDRP(bp) ((char*)(bp)-WSIZE) #define FTRP(bp) ((char*)(bp)+GET_SIZE(HDRP(bp))-DSIZE)
#define NEXT_BLKP(bp) ((char*)(bp)+GET_SIZE(((char*)(bp)-WSIZE))) #define PREV_BLKP(bp) ((char*)(bp)-GET_SIZE(((char*)(bp)-DSIZE)))
|
mm_init
在调用mm_malloc或者mm_free之前,必须用过mm_init函数来初始化堆。如果分配成功就返回0,否则返回-1。mm_init函数从内存系统得到四个字,并将它们初始化,创建一个空的空闲链表。然后调用extend_heap函数将堆扩展CHUNKSIZE字节,并创建初始的空闲块。
隐式空闲链表如图所示。第一个字是一个双字对齐的不使用的填充字,填充后面跟一个序言块(一个八字节的已分配块),只由头和尾组成。序言块在初始化时创建,并且永不释放。序言块后面是零个或者多个已块。结尾有一个特殊的结尾块,这个块是一个大小为零的已分配块,并且只由一个头部组成。分配器使用一个私有全局变量heap_list指向序言块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int mm_init(void) { if((heap_list=mem_sbrk(4*WSIZE))==(void*)-1) return -1; PUT(heap_list,0); PUT(heap_list+(1*WSIZE),PACK(DSIZE,1)); PUT(heap_list+(2*WSIZE),PACK(DSIZE,1)); PUT(heap_list+(3*WSIZE),PACK(0,1)); heap_list+=(2*WSIZE); if(extend_heap(CHUNKSIZE/WSIZE)==NULL) return -1;
return 0; }
|
extend_heap函数会在两种不同的环境中被调用:1.当堆被初始化时;2.当mm_malloc不能找到一个合适的匹配块时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static void *extend_heap(size_t words) { char *bp; size_t size; size=(words%2)?(words+1)*WSIZE:words*WSIZE; if((long)(bp=mem_sbrk(size))==-1) return NULL; PUT(HDRP(bp),PACK(size,0)); PUT(FTRP(bp),PACK(size,0)); PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1)); return coalesce(bp); }
|
最后的合并书中实现的是隐式空闲链表的立即合并策略
有且仅有四种情况:
- 前面已分配、后面已分配
- 前面已分配、后面为分配
- 前面未分配、后面已分配
- 前面未分配、后面未分配

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| static void *coalesce(void *bp) { size_t prev_alloc=GET_ALLOC(FTRP(PREV_BLKP(bp))); size_t next_alloc=GET_ALLOC(HDRP(NEXT_BLKP(bp))); size_t size=GET_SIZE(HDRP(bp)); if(prev_alloc && next_alloc) { return bp; } else if(prev_alloc && !next_alloc) { size+=GET_SIZE(HDRP(NEXT_BLKP(bp))); PUT(HDRP(bp),PACK(size,0)); PUT(FTRP(bp),PACK(size,0)); } else if(!prev_alloc && next_alloc) { size+=GET_SIZE(HDRP(PREV_BLKP(bp))); PUT(FTRP(bp),PACK(size,0)); PUT(HDRP(PREV_BLKP(bp)),PACK(size,0)); bp=PREV_BLKP(bp); } else if(!prev_alloc && !next_alloc) { size+=(GET_SIZE(HDRP(PREV_BLKP(bp)))+GET_SIZE(HDRP(NEXT_BLKP(bp)))); PUT(HDRP(PREV_BLKP(bp)),PACK(size,0)); PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0)); bp=PREV_BLKP(bp); } return bp; }
|
mm_free
将指定块释放,也就是将它的标记为改为未分配,在最后再进行一次合并
1 2 3 4 5 6 7
| void mm_free(void *bp) { size_t size=GET_SIZE(HDRP(bp)); PUT(HDRP(bp),PACK(size,0)); PUT(FTRP(bp),PACK(size,0)); coalesce(bp); }
|
mm_malloc
- 判断参数是否合法
- 计算实际分配大小
- 寻找合适的块,更改头部和尾部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void *mm_malloc(size_t size) { size_t asize; size_t extendsize; char *bp; if(size==0) return NULL; if(size<=DSIZE) asize=2*DSIZE; else asize=DSIZE*((size+(DSIZE)+(DSIZE-1))/DSIZE); if((bp=find_fit(asize))!=NULL) { place(bp,asize); return bp; } extendsize=MAX(asize,CHUNKSIZE); if((bp=extend_heap(extendsize/WSIZE))==NULL) return NULL; place(bp,asize); return bp; }
|
如果不能发现一个匹配的块,那么就扩展堆。
如何实现遍历隐式空闲链表?需要注意的是要适配首次搜索。首次搜索的时候只有一个块,即堆扩展产生的一个1024字的大块。要用到heap_list遍历,首次搜索的时候他还指向第二个序言块。

1 2 3 4 5 6 7 8 9 10 11 12
| static void* find_fit(size_t asize) { void* bp; for(bp=heap_list;GET_SIZE(HDRP(bp))>0;bp=NEXT_BLKP(bp)) { if(!GET_ALLOC(HDRP(bp)) && (GET_SIZE(HDRP(bp))>=asize)) { return bp; } } return NULL; }
|
place函数要实现的功能:设置头部,如果剩余块的大小大于最小块即四字,则进行分割
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static void place(void* bp,size_t asize) { size_t size=GET_SIZE(HDRP(bp)); if(size-asize>=(2*DSIZE)) { PUT(HDRP(bp),PACK(asize,1)); PUT(FTRP(bp),PACK(asize,1)); bp=NEXT_BLKP(bp); PUT(HDRP(bp),PACK(size-asize,0)); PUT(FTRP(bp),PACK(size-asize,0)); } else { PUT(HDRP(bp),PACK(size,1)); PUT(FTRP(bp),PACK(size,1)); } }
|
mm_realloc
1
| void *realloc(void *ptr, size_t size)
|
该函数返回一个指针,指向重新分配的地址。如果失败则返回NULL。
如果ptr为NULL,则会分配一个指定大小的块并返回一个指向它的指针。
如果ptr不为NULL,那么ptr就是指向一个需要重新分配的内存块,此时判断size,size为指定重新分配的大小,如果size为0那么释放该块并返回一个空指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void *mm_realloc(void *ptr, size_t size) { void *newptr=NULL; newptr=mm_malloc(size); if(!ptr) { printf("ptr为NULL,直接分配\n"); return newptr; } size_t copy_size=size; size_t before_size=GET_SIZE(HDRP(ptr)); if(size>before_size) { copy_size=before_size; } memcpy(newptr,ptr,copy_size); mm_free(ptr); return newptr; }
|
调试策略
可以使用printf辅助调试,可以在关键代码附近打印相关数值,比如在最后的realloc实现中刚开始有些问题,导致测试不通过,提示信息是未能将原块内容复制到新块。然后我就在这里面插了个printf打印copysize,发现打印的结果为copy_size=0,然后就发现问题出现在before那里,使用GET_SIZE(bp)获取了块的大小,因为bp指向的是有效载荷而不是头部,所以正确的做法就是改为GET_SIZE(HDRP(bp))。
测试优化
1 2 3
| make //编译生成驱动程序
./mdriver -V //-V参数展示详细信息
|

通过上述代码,实现了一个基于隐式空闲链表、采用首次适配、立即合并策略的分配器,得了72分。
优化以后再研究,现在实在没什么兴趣。。。。。 看见空间利用率、吞吐量我就头疼。。。。