前情提要
- 一定要把书中关于隐式空闲链表的实现代码好好看一下, 这个lab的代码和那个非常像
实验文档
- 只需要修改
mm.c
文件 mdriver.c
可以用来衡量性能make
./mdriver -V
- 需要完成的函数
int mm_init(void)
- 最开始的时候调用
- 失败时返回-1,成功时返回0
void *mm_malloc(size_t size)
- 返回堆上的一个地址
- 至少包含size字节
- 向8字节对齐
void mm_free(void *ptr)
- 这个地址是通过malloc或者realloc分配的
- 这个地址还没有被free
void *mm_realloc(void *ptr, size_t size)
- 如果ptr是null,则相当于调用malloc
- 如果size是0,相当于调用free
- 新的地址开始的内容需要和原来一样,但是大小由size决定
- 可以调用的函数
void *mem sbrk(int incr)
- 为堆开辟内存空间
- 返回新开辟的内存的起始地址
void *mem heap lo(void)
:返回堆的起始位置void *mem heap hi(void)
:返回堆的终止位置size t mem heapsize(void)
返回堆的大小size t mem pagesize(void)
返回内存页面的大小
- 测试
- ./mdriver -t traces/ -v
设计思路
- 模型选择书中的第603页的显示空闲链表
- 首部和尾部相同,记录 s i z e ∣ a l l o c size | alloc size∣alloc
- 首部后面紧跟着两个指针,分别是pre和next,这两个指针指向前后的空闲块。(其实我这里有个疑惑,按理说我们操作的地址都是64位的,怎么可以用一块表示一个地址,一块明明才32位)
- 空闲链表的管理采用书中9.9.14节介绍的方法
- 将空闲块按大小划分为16类,分别是大小为 1 , 2 − 3 , 4 − 7 , 8 − 15 1,2-3,4-7,8-15 1,2−3,4−7,8−15等等,最后一块记录的是 2 15 − 无穷 2^{15}-无穷 215−无穷。每一类都维护一个链表,链表从小到大地记录着当前存在且位于这一类大小范围内的空闲块的bp指针。
- 查找的方法类似于二分查找,找到大于等于当前块的类,然后从这个类开始找,如果找不到合适的块,就往下一类找。如果实在找不到,就要去扩展堆的大小了
mm_init
int mm_init(void)
这个函数是用来初始化我们的堆,
- 初始化空闲块数组
- 初始化原始的堆
- 申请4个块(16个字节),和书中的隐式链表一样
- 分别为这4个块设置size和alloc
- 申请扩展堆
- (构建初始的空闲块)
- 这里使用一个函数实现,因为后续还会使用到扩展堆这个操作
具体实现如下:
int mm_init(void) {
// 初始化记录空闲块的数组
for (int i = 0; i < LIST_MAX_SIZE; i++) {
segregated_free_lists[i] = NULL;
}
// 先请求空间来初始化堆的结构
void *heap_listp;
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));
// 为堆申请一个chunksize的虚存空间
if (extend_heap(CHUNKSIZE) == NULL) {
return -1;
}
return 0;
}
extend_heap
static void *extend_heap(size_t size)
这个函数用来给堆扩展size大小的空间,要完成的功能如下所示
- 首先调整size:
#define ALIGN(size) (((size) + (ALIGNMENT - 1)) & ~0x7)
- 这个宏就是实现了将size往最近的8的倍数舍入
- 使用
mem_sbrk
申请size大小的空闲块 - 设置新得到的空闲块的的首部和尾部,以及重新设置堆的尾部
- 新申请的空闲块的前一个4字节的小块就是之前堆的尾部,即长度为0,分配为为1的那个哥们。现在直接修改这个哥们,使其成为新申请的块的首部。
- 将这个空闲块给放到空闲链表中去
- 尝试合并这个空闲块,因为原来堆的后面可能就是空闲的内存块
具体实现如下所示:
static void *extend_heap(size_t size) {
// word_count必须是偶数
size = ALIGN(size);
// 使用mem_sbrk去申请空间
void *bp;
if ((bp = mem_sbrk(size)) == (void *)-1) {
return NULL;
}
// 将新申请的内存加到已有的堆上去
// 现在有个指针bp,它指向的新申请的内存的真正的起始位置
// 正常来说,这个bp的前一块就是之前堆的结尾块,是没用的,现在直接拿来做新申请的块的首部
// 然后又将新申请的块的最后一块变成堆的尾部
PUT(HDRP(bp), PACK(size, 0));
PUT(FTRP(bp), PACK(size, 0));
PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1));
// 将这个新的空闲块插入到我们的列表里
insert_node(bp, size);
return coalesce(bp);
}
insert_node
static void insert_node(void *ptr, size_t size)
这个函数是将以ptr为bp指针的空闲块给塞到空闲块数组中去,bp指针指的是当前空闲块的有效载荷的第一个字节的地址,也就是头部+WSIZE
- 首先找到这个size应该对应数组的哪个下标。(数组的每个下标存储的链表)
- 拿到这个下标对应的链表,根据size找到应该插入到这个链表的哪个位置(链表是从小大递增的)
- 分情况插入
- 当前链表为空
- 在首部插入
- 在尾部插入
- 在链表中间插入
这里具体的实现上就用了一些书上没有的宏定义,有点晦涩,所以解释一下。
SET_PTR
#define SET_PTR(p, ptr) (*(unsigned int *)(p) = (unsigned int)(ptr))
- 将地址p看做指向一个无符号int型整数,即一个32位数的地址,然后将地址ptr看做是一个32位数,即将地址ptr放到地址p指向的位置。
- 那么以后
*(unsigned int *)(p)
就可以得到ptr。 - 这个操作是用来存储这个空闲块的pre和next指针用的。p要么是pre指针所在的地址,要么是next指针所在的地址。然后
*(unsigned int *)(p)
就可以得到pre指针或者next指针
GET_PRE_PTR
#define GET_PRE_PTR(ptr) ((void *)(ptr))
- 这个宏传入一个ptr,这个ptr往往就是一个空闲块的bp指针。通过这个宏,可以得到这个空闲块存储pre指针的位置。
- 其实这一波操作之后返回的就是ptr自己,因为说过了,调用这个宏时,ptr往往传入的就是bp,而bp恰好就是空闲块中指向pre指针的指针。
- 之所以要做这种脱裤子放屁的事情,一个是为了使用起来更加清晰没有歧义,另一方面是为了和
GET_SUCC_PTR
的使用统一起来 #define GET_SUCC_PTR(ptr) ((void *)(ptr) + WSIZE)
就获得了指向next指针的指针
GET_PRE
#define GET_PRE(ptr) (*(void **)(GET_PRE_PTR(ptr)))
- 这个宏传入一个ptr,这个ptr其实往往就是某个空闲块的bp指针,然后返回这个空闲块存储的pre指针。
- 注意了
GET_PRE_PTR
实际上是指向pre指针的指针,但是呢,因为我们前面的定义中返回的void*
,因此,我们需要先将它强制转为void**
类型,即指向指针的指针。然后再通过*
操作取出来pre空闲块真正的地址。 #define GET_SUCC(ptr) (*(void **)(GET_SUCC_PTR(ptr)))
就是取得真正的succ指针
这玩意是有点恶心的,对照着下面的代码看,应该就明白了
static void insert_node(void *ptr, size_t size) {
// 首先找到这个size应该在哪个格子里
int pos = find_pos(size);
// 扫描这个格子里存的链表,找到应该插入的位置
void *pre_ptr = NULL;
void *cur_ptr = segregated_free_lists[pos];
while (cur_ptr != NULL && size > GET_SIZE(HDRP(cur_ptr))) {
pre_ptr = cur_ptr;
cur_ptr = GET_SUCC(cur_ptr);
}
// 分情况讨论,正常来说,我们应该插入pre_ptr和cur_ptr之间
// 前驱比自己小,后续比自己大
// 如果pre_ptr为空
if (pre_ptr == NULL) {
// 如果cur_ptr也为空,说明这个链表就是空的,直接插入即可
if (cur_ptr == NULL) {
segregated_free_lists[pos] = ptr;
SET_PTR(GET_PRE_PTR(ptr), NULL);
SET_PTR(GET_SUCC_PTR(ptr), NULL);
} else {
// cur_ptr不为空,说明要插入的是第一个位置
segregated_free_lists[pos] = ptr;
SET_PTR(GET_PRE_PTR(cur_ptr), ptr);
SET_PTR(GET_SUCC_PTR(ptr), cur_ptr);
SET_PTR(GET_PRE_PTR(ptr), NULL);
}
// pre_ptr不为空
} else {
// 如果cur_ptr为空,说明是在链表尾部插入
if (cur_ptr == NULL) {
SET_PTR(GET_SUCC_PTR(pre_ptr), ptr);
SET_PTR(GET_PRE_PTR(ptr), pre_ptr);
SET_PTR(GET_SUCC_PTR(ptr), NULL);
} else {
// 如果cur_ptr不为空,说明是在链表中间插入
SET_PTR(GET_SUCC_PTR(pre_ptr), ptr);
SET_PTR(GET_PRE_PTR(ptr), pre_ptr);
SET_PTR(GET_SUCC_PTR(ptr), cur_ptr);
SET_PTR(GET_PRE_PTR(cur_ptr), ptr);
}
}
}
coalesce
static void *coalesce(void *bp)
- 这个函数的作用是将bp指向的空闲块和前后的空闲块合并,但是前后的空闲块也不一定存在,所以要分类讨论
- 这个函数返回合并之后的空闲块的起始地址,这个起始地址只会在前面存在空闲块的情况下返回前面空闲块的起始地址,否则就是返回当前的bp
具体实现思路 - 首先分别获取前面和后面的内存块是否被分配(地址意义上的前后内存块,而不是空闲内存块那个意义上的)
- 分类讨论,该合并合并,如果需要合并
- 先将参与合并的空闲内存块在空闲内存数组中给删了
- 然后设置合并之后的空闲内存块的首部和尾部
- 最后给它加入到空闲内存数组中去
static void *coalesce(void *bp) {
// 首先获取前后内存块的状态
int is_pre_alloc = GET_ALLOC(HDRP(PREV_BLKP(bp)));
int pre_size = GET_SIZE(HDRP(PREV_BLKP(bp)));
int is_next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
int next_size = GET_SIZE(HDRP(NEXT_BLKP(bp)));
int cur_size = GET_SIZE(HDRP(bp));
// 根据状态分类讨论
int new_size;
// 前后均分配了
if (is_pre_alloc && is_next_alloc) {
return bp;
} else if (is_pre_alloc && !is_next_alloc) {
// 前面分配了,后面没分配,所以和后面合并
// 首先,在空闲块数组中删除这两个块
delete_node(bp);
delete_node(NEXT_BLKP(bp));
// 然后修改首部和尾部
new_size = cur_size + next_size;
} else if (!is_pre_alloc && is_next_alloc) {
delete_node(bp);
delete_node(PREV_BLKP(bp));
new_size = pre_size + cur_size;
// 修改当前的bp,因为和前面合并了,现在的bp应该指向前面的块的头部
bp = PREV_BLKP(bp);
} else {
// 前后都是空的
delete_node(bp);
delete_node(PREV_BLKP(bp));
delete_node(NEXT_BLKP(bp));
new_size = pre_size + cur_size + next_size;
bp = PREV_BLKP(bp);
}
// 修改当前合并后的空闲块的首部和尾部
PUT(HDRP(bp), PACK(new_size, 0));
// 只要设置好了头部,那么尾部就可以直接操作
PUT(FTRP(bp), PACK(new_size, 0));
// 将这个空闲块插入空闲块数组
insert_node(bp, new_size);
return bp;
}
mm_malloc
void *mm_malloc(size_t size)
- 这个函数是分配一个size大小的内存块,并返回这个内存块的bp指针
实现思路 - 首先要特判size不合法的情况,小于等于0
- 然后要调整size
- 如果size不足最小块的要求,将size变为最小快的长度。最小快应该是
2*DSIZE
- 如果size不为8的倍数,修改为8个倍数。之所以要修改为8个倍数,是为了对齐的要求。对齐是为了取内存数据的时候,一次可以成功取出来,避免出现一个数据存在于两个cache行的情况
- 如果size不足最小块的要求,将size变为最小快的长度。最小快应该是
- 根据size去空闲内存数组中去找是否有合适的内存块。可能size对应的那个链表中找不到,那就去索引更大的链表中找
- 如果实在找不到,那就要申请扩展堆
- 最后修改找到的空闲块(这一步通过
place
操作完成)
void *mm_malloc(size_t size) {
// 首先调整size为合法值,最小为2*DSIZE,否则一定要是8的倍数
if (size == 0) {
return NULL;
} else if (size <= DSIZE) {
size = 2 * DSIZE;
} else {
size = ALIGN(size + DSIZE);
}
// 根据size去空闲块数组里找最合适的那个
int pos = find_pos(size);
void *fit_ptr = NULL;
while (pos < LIST_MAX_SIZE) {
// 去当前项里面找
void *cur_ptr = segregated_free_lists[pos];
while (cur_ptr != NULL) {
if (GET_SIZE(HDRP(cur_ptr)) < size) {
cur_ptr = GET_SUCC(cur_ptr);
} else {
fit_ptr = cur_ptr;
break;
}
}
if (fit_ptr != NULL) {
break;
}
pos++;
}
// 如果没有足够大的,说明堆要扩充空间了
if (fit_ptr == NULL) {
if ((fit_ptr = extend_heap(MAX(size, CHUNKSIZE))) == NULL) {
return NULL;
}
}
// 在该空闲块中分配size大小的块
fit_ptr = place(fit_ptr, size);
return fit_ptr;
}
place
static void *place(void *bp, size_t size)
- 这个函数是将bp所指向的内存块分配size大小的内存走,即留下原大小减去size大小的内存块
- 这里用了一个很神奇的优化方法。不是一直把这个空闲块的前size个字节给分配掉,而是根据size的大小讨论,如果比较小就分配前size个字节,如果比较大就分配后size个字节。之所以这样优化,是为了避免一些极端情况下出现很多外部内存碎片
- 分配的操作的实现其实是通过设置首部和尾部实现的,分别设置前部和后部的首尾部,就完成了分配的操作,然后将新生成的小空闲内存块给加入到空闲内存数组中去
实现思路 - 如果剩下的空闲块不足一个最小空闲块,那就直接全部分配
- 如果size比较大,分配后size个字节,这个比较大是一个经验值,选一个效果比较好就行了
- 如果size比较小,分配前size个字节
static void *place(void *bp, size_t size) {
// 在bp中分配size大小的空闲块走
// 先在数组中删除bp空闲块
delete_node(bp);
// 获得bp块的长度
size_t free_size = GET_SIZE(HDRP(bp));
// 如果剩下的小于2*DSIZE,那就不用再插入回去了
if (free_size - size < 2 * DSIZE) {
PUT(HDRP(bp), PACK(free_size, 1));
PUT(FTRP(bp), PACK(free_size, 1));
} else if (size >= 96) {
PUT(HDRP(bp), PACK(free_size - size, 0));
PUT(FTRP(bp), PACK(free_size - size, 0));
insert_node(bp, free_size - size);
bp = NEXT_BLKP(bp);
PUT(HDRP(bp), PACK(size, 1));
PUT(FTRP(bp), PACK(size, 1));
} else {
// 注意,这里是把前半部分给分配了,不能修改bp,最后还是要返回bp
// 前半部分要分配,后半部分重新插入
PUT(HDRP(bp), PACK(size, 1));
PUT(FTRP(bp), PACK(size, 1));
// 修改bp指向后半段
PUT(HDRP(NEXT_BLKP(bp)), PACK(free_size - size, 0));
PUT(FTRP(NEXT_BLKP(bp)), PACK(free_size - size, 0));
// 重新放入空闲块数组
insert_node(NEXT_BLKP(bp), free_size - size);
}
return bp;
}
free
void mm_free(void *ptr)
- 这个函数是将ptr指向的空闲内存块给free掉
实现思路 - 首先设置首尾部,将其alloc的状态改为0
- 然后加入空闲块链表
- 最后尝试空闲块的合并
其实到这里就可以发现,设置一个块就只需要修改首部和尾部,空闲块再丢入空闲块数组并尝试合并即可
void mm_free(void *ptr) {
// 修改标志位
size_t size = GET_SIZE(HDRP(ptr));
PUT(HDRP(ptr), PACK(size, 0));
PUT(FTRP(ptr), PACK(size, 0));
// 插入空闲列表
insert_node(ptr, size);
// 尝试合并
coalesce(ptr);
}
mm_realloc
void *mm_realloc(void *ptr, size_t size)
- 这个函数是将ptr指向的块给扩展为size大小
- 这里有个优化的思路就是能不copy就尽量不copy,因为如果要copy那就会引起复制以及free当前块。所以最好能和后面的内存块合并。
实现思路 - 首先需要处理size非法的情况
- 为0啊
- 不足最小块的大小的要求啊
- 向8对齐啊
- 如果size小于当前空闲块大小,则直接忽略
- 如果这个块是堆的尾部,那么可以试着去扩展堆,这样可以减少copy的操作,但是这个优化可有可无吧只能说
- 如果这个块后面是一个空闲块,可以将这个空闲块并入当前块,看看是否可以满足要求,如果可以的话就合并,然后返回答案
- 如果上面都不行,那就只能重新malloc然后memcpy,再free掉当前内存块了
写博客的时候才发现我下面的实现有几个问题 - 扩展堆尾部以及和后面的块合并的时候,如果成功了,那是不是给这个块扩的太多了?超过了它需要的size的要求
- 我依稀记得实验文档好像要求可以实现缩小,即size是可以小于当前空闲块大小的,但是我这里是直接忽略了
- 上面两个问题好像不对导致结果错误,但是会牺牲空间,并且第2点会带来一点安全隐患,因为我并没有真正的缩小块,用户依然可以访问。只能希望用户在realloc之后,就按他想要的size来操作这个内存块了。
- 但是太懒了,不想实现了。
代码实现
void *mm_realloc(void *ptr, size_t size) {
// 首先检查size的合法性
if (size == 0) {
return NULL;
}
// 修改size使其对齐
if (size <= DSIZE) {
size = 2 * DSIZE;
} else {
size = ALIGN(size + DSIZE);
}
// 计算当前块的大小与要求的size的差值
int cur_size = GET_SIZE(HDRP(ptr));
int change_size = cur_size - size;
// 如果size小于等于当前长度,则不需要重新分配
if (change_size >= 0) {
return ptr;
}
// 如果当前块后面就是结尾
if (GET_SIZE(HDRP(NEXT_BLKP(ptr))) == 0) {
// 扩展
if (extend_heap(MAX(change_size, CHUNKSIZE)) == NULL) {
return NULL;
}
// 扩展成功,修改头尾部
delete_node(NEXT_BLKP(ptr));
PUT(HDRP(ptr), PACK(cur_size + MAX(change_size, CHUNKSIZE), 1));
PUT(FTRP(ptr), PACK(cur_size + MAX(change_size, CHUNKSIZE), 1));
return ptr;
}
// 如果当前块后面有个free块,尝试去合并,看看是不是可以
if (!GET_ALLOC(HDRP(NEXT_BLKP(ptr)))) {
// 如果加起来长度够的话
int next_size = GET_SIZE(HDRP(NEXT_BLKP(ptr)));
if (cur_size + next_size >= size) {
delete_node(NEXT_BLKP(ptr));
PUT(HDRP(ptr), PACK(cur_size + next_size, 1));
PUT(FTRP(ptr), PACK(cur_size + next_size, 1));
return ptr;
}
}
// 最后一步了,只能去重新申请
void *new_ptr = mm_malloc(size);
memcpy(new_ptr, ptr, cur_size);
mm_free(ptr);
return new_ptr;
}
出现的错误
- 死循环:忘记给pos++
- 段错误:place的时候,没有正确返回bp指针
- 不知名错误:在realocate的时候,在尾部重新申请位置的情况下,忘记把新申请的从空闲块里删除了