0x1 介绍 可以从手册得知,我们需要修改mm.c里面的三个函数mm_malloc
、mm_free
、mm_realloc
来实现堆的相关功能。教师们为我们编写了如此庞大的测试环境,那么我们也要认真完成它。
由于lab自带的版本是每次请求时都执行sbrk系统调用。我们知道,系统调用会是陷入内核,花费大量时间,那么接下来我们要做的就是减少系统调用的次数。
下图为自带版本的测试,可以看到内存利用率并不高 ,并且有部分测试并未通过。
0x2 隐式空闲列表 我们进行第一次修改,为其实现一个简单的分配器。
注意事项:
返回值
描述
(void *)-1
调用失败,无法扩展数据段。通常伴随设置errno
来描述具体错误。
非空指针
调用成功,返回指向数据段新分配空间的指针。
首先是宏定义,宏定义可以简化操作,避免一些难以察觉的错误。
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 #define ALIGNMENT 8 #define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~0x7) #define SIZE_T_SIZE (ALIGN(sizeof(size_t))) #define WSIZE 4 #define DSIZE 8 #define CHUNKSIZE (1 << 12) #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define MIN(a, b) ((a) < (b) ? (a) : (b)) #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)) #define PACK(size, alloc) ((size) | (alloc)) static char *heap_listp = 0 ;
手册提到,我们可以实现mm_check来检查内存分配器是否在工作。
这里我实现了两个日志函数,log_message_function
用来记录函数执行顺序,mm_check
用来打印空闲链表,这里的日志会严重影响运行速度,于是使用宏来控制日志的开关。
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 36 37 38 39 #ifdef DEBUG #define log_message(fmt, ...) log_message_function(fmt, __VA_ARGS__) #define mm_check() mm_check_function() #else #define log_message(fmt, ...) #define mm_check() #endif static inline void log_message_function (const char *fmt, ...) { FILE *file; file = fopen("log.log" , "a" ); va_list args; va_start(args, fmt); vfprintf (file, fmt, args); va_end(args); fclose(file); } static inline void mm_check_function () { FILE *file; file = fopen("log.log" , "a" ); if (file == NULL ) exit (0 ); fprintf (file, "---------------------链表----------------------" ); fprintf (file, "\n" ); char *p = heap_listp; while (GET_SIZE(HDRP(p)) != 0 ) { size_t size = GET_SIZE(HDRP(p)); size_t alloc = GET_ALLOC(HDRP(p)); fprintf (file, "pointer:%p size:%d alloc:%d\n" , p, size, alloc); p = NEXT_BLKP(p); } fprintf (file, "-----------------------------------------------" ); fprintf (file, "\n" ); if (fclose(file) != 0 ) { exit (0 ); } }
初始化堆空间,主要是分配头部和脚部,使其符合空闲链表的组织方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int mm_init (void ) { if ((heap_listp = mem_sbrk(4 * WSIZE)) == (void *)-1 ) return -1 ; PUT(heap_listp, 0 ); PUT(heap_listp + (1 * WSIZE), PACK(DSIZE, 1 )); PUT(heap_listp + (2 * WSIZE), PACK(DSIZE, 1 )); PUT(heap_listp + (3 * WSIZE), PACK(0 , 1 )); heap_listp += (4 * WSIZE); if (extend_heap(CHUNKSIZE / WSIZE) == NULL ) return -1 ; return 0 ; }
mm_malloc :首先要将要分配的空间大小进行8字节对齐,调用find_fit
函数:
当有空闲空间时,调用place()
函数,将此块标记为已分配。
当无空闲空间时,调用 extend_heap
函数,增加并合并堆内存,然后调用place()
函数。
mm_free:
将已分配的块标记为空闲块,然后调用coalesce()
函数,按照下图四种情况进行合并。
mm_realloc:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 void *mm_malloc (size_t size) { log_message("malloc %d\n" , size); if (size == 0 ) return NULL ; size_t asize = ALIGN(size + SIZE_T_SIZE); size_t extendsize; char *bp; if ((bp = find_fit(asize)) != NULL ) { place(bp, asize); log_message("malloc:success! pointer:%p size:%d alloc:%d \n\n" , bp, GET_SIZE(HDRP(bp)), GET_ALLOC(HDRP(bp))); mm_check(); return bp; } else { extendsize = MAX(CHUNKSIZE, asize); log_message("We need %d!!!\n" , extendsize); if ((bp = extend_heap(extendsize / WSIZE)) == NULL ) return NULL ; else { place(bp, asize); log_message("malloc:success! pointer:%p size:%d alloc:%d \n\n" , bp, GET_SIZE(HDRP(bp)), GET_ALLOC(HDRP(bp))); mm_check(); return bp; } } } void mm_free (void *ptr) { if (ptr == NULL ) return ; size_t size = GET_SIZE(HDRP(ptr)); PUT(HDRP(ptr), PACK(size, 0 )); PUT(FTRP(ptr), PACK(size, 0 )); coalesce(ptr); } void *mm_realloc (void *ptr, size_t size) { size_t oldsize; void *newptr; if (size == 0 ) { mm_free(ptr); return 0 ; } if (ptr == NULL ) { return mm_malloc(size); } newptr = mm_malloc(size); if (!newptr) { return 0 ; } oldsize = GET_SIZE(HDRP(ptr)); if (size < oldsize) oldsize = size; memcpy (newptr, ptr, oldsize); mm_free(ptr); return newptr; }
接下来的则是各种子函数,主要是注意堆操作的方式,仔细检查。在这里,log_message()可以帮助我们调试,快速确定合并方式,执行结果,函数执行顺序等重要方式。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 static inline void *coalesce (void *bp) { size_t prev_alloc = GET_ALLOC(HDRP(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) { log_message("coalesce: case 1 pointer:%p\n" , bp); 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 )); log_message("coalesce: case 2 pointer:%p\n" , bp); } 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); log_message("coalesce: case 3 pointer:%p\n" , bp); } else if (!prev_alloc && !next_alloc) { size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp))); PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0 )); PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0 )); log_message("coalesce: case 4 pointer:%p\n" , bp); } return bp; } static inline char *find_fit (size_t asize) { char *p = heap_listp; while (GET_SIZE(HDRP(p)) != 0 ) { size_t size = GET_SIZE(HDRP(p)); size_t alloc = GET_ALLOC(HDRP(p)); if (!alloc && size >= asize) { log_message("fit_block poniter:%p size:%d alloc:%d\n" , p, size, alloc); return p; } p = NEXT_BLKP(p); } return NULL ; } static inline void place (char *ptr, size_t asize) { size_t size = GET_SIZE(HDRP(ptr)); if ((size - asize) >= (2 * DSIZE)) { PUT(HDRP(ptr), PACK(asize, 1 )); PUT(FTRP(ptr), PACK(asize, 1 )); ptr = NEXT_BLKP(ptr); PUT(HDRP(ptr), PACK(size - asize, 0 )); PUT(FTRP(ptr), PACK(size - asize, 0 )); } else { PUT(HDRP(ptr), PACK(size, 1 )); PUT(FTRP(ptr), PACK(size, 1 )); } } static inline 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 )); log_message("extend_heap: pointer:%p size:%d \n" , bp, size); return coalesce(bp); }
让我们来测试一下,可以看到我们的分数成功来到了64分!
0x3 显式空闲列表 接下来进行我们的改进!在空闲块
内添加前驱后继指针,当需要空闲块时,直接通过指针直接访问下一节点,而不是需要先获取头部信息计算偏移。这种方法可以加快分配速度,但是在释放块时需要线性时间来搜索前驱。那么这样到底可以提升多少访问速度呢,我们使用后进先出的LIFO顺序维护链表。
利用宏定义来快速获取和放置前驱和后继指针, 同时定义插入和删除函数,简化对链表的操作。
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 36 #define GET_PRED(bp) (*(char **)(bp)) #define GET_SUCC(bp) (*(char **)((char *)(bp) + WSIZE)) #define PUT_PRED(bp, pred) (*(char **)(bp) = (pred)) #define PUT_SUCC(bp, succ) (*(char **)((char *)(bp) + WSIZE) = (succ)) static char *free_listp = NULL ;static void insert_free_block (char *bp) { if (free_listp != NULL ) { PUT_PRED(bp, NULL ); PUT_SUCC(bp, free_listp); PUT_PRED(free_listp, bp); } else { PUT_PRED(bp, NULL ); PUT_SUCC(bp, NULL ); } free_listp = bp; } static void remove_free_block (char *bp) { char *pred = GET_PRED(bp); char *succ = GET_SUCC(bp); if (pred != NULL ) { PUT_SUCC(pred, succ); } else { free_listp = succ; } if (succ != NULL ) { PUT_PRED(succ, pred); } }
重写mm_check_function()
检测空闲链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void mm_check_function () { FILE *file; file = fopen("log.log" , "a" ); char *p = free_listp; fprintf (file, "\n\n====================空闲链表======================\n" ); while (p != NULL ) { size_t size = GET_SIZE(HDRP(p)); size_t alloc = GET_ALLOC(HDRP(p)); fprintf (file, "pointer:%p pre:%p succ:%p size:%d alloc:%d\n" , p,GET_PRED(p), GET_SUCC(p), size, alloc); p = GET_SUCC(p); } fprintf (file, "\n====================空闲链表======================\n\n\n" ); if (fclose(file) != 0 ) { exit (0 ); } }
实现的三大函数只需在free时将空闲块加入空闲链表即可。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 int mm_init (void ) { if ((heap_listp = mem_sbrk(4 * WSIZE)) == (void *)-1 ) return -1 ; PUT(heap_listp, 0 ); PUT(heap_listp + (1 * WSIZE), PACK(DSIZE, 1 )); PUT(heap_listp + (2 * WSIZE), PACK(DSIZE, 1 )); PUT(heap_listp + (3 * WSIZE), PACK(0 , 1 )); heap_listp += (4 * WSIZE); free_listp = NULL ; if (extend_heap(CHUNKSIZE / WSIZE) == NULL ) return -1 ; return 0 ; } void *mm_malloc (size_t size) { log_message("malloc %d\n" , size); if (size == 0 ) return NULL ; size_t asize = ALIGN(size + SIZE_T_SIZE); size_t extendsize; char *bp; if ((bp = find_fit(asize)) != NULL ) { place(bp, asize); log_message("malloc:success! pointer:%p size:%d alloc:%d \n\n" , bp, GET_SIZE(HDRP(bp)), GET_ALLOC(HDRP(bp))); mm_check(); return bp; } else { extendsize = MAX(CHUNKSIZE, asize); log_message("We need %d!!!\n" , extendsize); if ((bp = extend_heap(extendsize / WSIZE)) == NULL ) return NULL ; else { place(bp, asize); log_message("malloc:success! pointer:%p size:%d alloc:%d \n\n" , bp, GET_SIZE(HDRP(bp)), GET_ALLOC(HDRP(bp))); mm_check(); return bp; } } } void mm_free (void *ptr) { if (ptr == NULL ) return ; size_t size = GET_SIZE(HDRP(ptr)); PUT(HDRP(ptr), PACK(size, 0 )); PUT(FTRP(ptr), PACK(size, 0 )); insert_free_block(ptr); coalesce(ptr); } void *mm_realloc (void *ptr, size_t size) { size_t oldsize; void *newptr; if (size == 0 ) { mm_free(ptr); return 0 ; } if (ptr == NULL ) { return mm_malloc(size); } newptr = mm_malloc(size); if (!newptr) { return 0 ; } oldsize = GET_SIZE(HDRP(ptr)); if (size < oldsize) oldsize = size; memcpy (newptr, ptr, oldsize); mm_free(ptr); return newptr; }
注意区分已分配的块和未分配的块,
只有未分配的块才需要添加前驱和后继指针,已分配的块需要及时去除前驱和后继指针。
coalesce函数不同情况的空闲块都需要先删除指针后重新分配。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 static inline void *coalesce (void *bp) { size_t prev_alloc = GET_ALLOC(HDRP(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) { log_message("coalesce: case 1 pointer:%p\n" , bp); return bp; } else if (prev_alloc && !next_alloc) { size += GET_SIZE(HDRP(NEXT_BLKP(bp))); remove_free_block(bp); remove_free_block(NEXT_BLKP(bp)); PUT(HDRP(bp), PACK(size, 0 )); PUT(FTRP(bp), PACK(size, 0 )); insert_free_block(bp); log_message("coalesce: case 2 pointer:%p\n" , bp); } else if (!prev_alloc && next_alloc) { remove_free_block(bp); remove_free_block(PREV_BLKP(bp)); size += GET_SIZE(HDRP(PREV_BLKP(bp))); PUT(FTRP(bp), PACK(size, 0 )); PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0 )); insert_free_block(PREV_BLKP(bp)); bp = PREV_BLKP(bp); log_message("coalesce: case 3 pointer:%p\n" , bp); } else if (!prev_alloc && !next_alloc) { remove_free_block(PREV_BLKP(bp)); remove_free_block(bp); remove_free_block(NEXT_BLKP(bp)); size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp))); PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0 )); PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0 )); bp = PREV_BLKP(bp); insert_free_block(bp); log_message("coalesce: case 4 pointer:%p\n" , bp); } return bp; } static inline char *find_fit (size_t asize) { char *p = free_listp; while (p != NULL ) { size_t size = GET_SIZE(HDRP(p)); size_t alloc = GET_ALLOC(HDRP(p)); if (!alloc && size >= asize) { log_message("fit_block poniter:%p size:%d alloc:%d\n" , p, size, alloc); return p; } p = GET_SUCC(p); } return NULL ; } static inline void place (char *ptr, size_t asize) { size_t size = GET_SIZE(HDRP(ptr)); if ((size - asize) >= (2 * DSIZE)) { PUT(HDRP(ptr), PACK(asize, 1 )); PUT(FTRP(ptr), PACK(asize, 1 )); remove_free_block(ptr); char *next_ptr = NEXT_BLKP(ptr); PUT(HDRP(next_ptr), PACK(size - asize, 0 )); PUT(FTRP(next_ptr), PACK(size - asize, 0 )); insert_free_block(next_ptr); } else { PUT(HDRP(ptr), PACK(size, 1 )); PUT(FTRP(ptr), PACK(size, 1 )); remove_free_block(ptr); } } static inline 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 )); log_message("extend_heap: pointer:%p size:%d \n" , bp, size); insert_free_block(bp); mm_check(); return coalesce(bp); }
可以看到虽然内存利用率有些许下降,但是我们分配速度得到了快速的提升!!!那么,82也算是优秀了吧(👉゚ヮ゚)👉。
0x4分离空闲列表 将不同大小的空闲列表分割,将分配时查找空闲块的时间变为匹配不同大小链表的时间,当空闲块很多时,可以显著提高分配器的效率。
这里先标记为TODO,希望以后有时间会回来做。