malloc & free的实现(malloc lab)

789 阅读13分钟

预备知识

零.

以下内容都是在虚拟内存的基础上,本来对malloc来说物理内存就是透明的

一.linux虚拟内存管理

概念:虚拟内存就是在虚拟存储器上的地址索引,而虚拟存储器借csapp的话是被组织为一个存放在磁盘上的N个连续的字节大小的数组。

虚拟地址器提供了一个机制(虚拟地址到物理地址的映射),让内存的地址空间大大增加,简单的说就是每一个进程一个存储器映像。(而虚拟地址到物理地址的映射借助了页表,这些分页,地址翻译的知识我就准备跳过了,因为这些对于malloc来说都是透明的,需要了解的可以去看csapp)

image-20221125185208424

(图来源于csapp原书)

那这个在代码上是怎么实现的呢?

首先我们要知道一件事,就是上文存储器映像中的段还有一个名字加vma(virtual memory area),有一个结构vm_area_struct来描述它。

在linux中用一种加task_struct的结果来描述进程,而task_struct中有一项mm来描述内存。

image-20221125221124002

(图来源于csapp原书)

区域被初始化后,os就会进行存储器映射

二.存储器映射

什么是存储器映射?存储器映射就是把vma与一个磁盘对象关联起来。一般来说存储器可以映射到普通文件和匿名文件(bss区域映射到匿名文件的)

事实上每一个vma被映射过后(或者说初始化后)就要装入swap分区,所以swap空间制约着正在运行的进程所能分配的虚拟页面的数量。

现在磁盘上的对象分为了两类:共享对象和私有对象。

每个进程都包含一些公有的东西比如lib库,如果每个进程放一份在内存中不就浪费了吗,所以就有了共享对象,让它映射到不同的进程中。对了大家都知道进程间通信可以通过shared memory来实现,也是共享对象的功劳。

image-20221125223038033

(图来源于csapp原书)

还有私有对象,不过它也有自己的秘诀来减小开销,那就是copy-on-write(写时拷贝)。

所谓写时拷贝就是在进程没有写的需求时,进程二与进程一共享,知道有一个进程要开始写时再分配,那么新分配的内存对原进程不可见

image-20221125223854062

(图来源于csapp原书)

我们的fork就用了这项技术,在父进程创建子进程时就如同上图的开始时,子进程共享父进程的内存,直到父进程或子进程开始写时,copy-on-write再开始创建新页面,因此每个进程就有了私有地址空间。那execve又干了什么?总体上干了四件事

1.删除已经存在的用户结构

2.映射私有area

2.映射共享area

4.设置pc,开始执行代码

另外说一句,所谓load并不是直接把文件放在内存里,而是完成映射后,再进程被调度时,os会发出page fault来换入代码和数据页面

三.mmap函数进行用户级存储器映射

unix进程可以使用mmap函数来创建新的vma并将对象映射过来

void *mmap(void * start,size_t lenth,int prot,int flags,int fd,off_t offset);

prot代表权限比如可写和可读

flag代表时共享还是私有

image-20221125225300482

(图来源于csapp原书)

动态存储器分配

虽然有mmap函数,但是c语言再运行时还是需要动态存储器分配的。动态存储器分配就是维护着heap

image-20221125234221078

而分配器将heap视为一组不同大小的block数组。

一般分配器分为两个风格:显示分配器,隐式分配器

显示分配器(本文把malloc等同于显示分配器)

隐式分配器(垃圾收集器)不在本文讨论中

显示分配器的实现要点(malloc)

前面说了这么多,终于到了malloc。不过malloc也有不同的实现方式,而区别他们的正是上文说的block的结构和组织方式。

malloc实现要点:

1.将heap中的空闲块组织成freelist

2.malloc 在freelist中找寻空闲块(如果没有,就调用sbrk函数分配),而在block链中查找合适的block有很多算法一般来说有first-fit和best-fit,分配后怎么做split的问题

3.在free时要注意块的合并

4.freelist的实现:隐式空闲链表,显示空闲双向链表,分离存储,红黑树。

image-20221203133846449

来源(27-dynamic-mem1.pptx (williams.edu)

freelist实现的不同也意味着block的不同,但一般来说block分为meta-data(data control block)和payload(看不懂可以看下面的block图)

隐式空闲链表block管理要点

接下来讲隐式空闲链表的block

image-20221127223502414

meta中存放着大小和分配情况,因为大小是以8bit对齐的,所以后面三位永远是000,所以用00a表示分配情况,a为0表空闲,相反a为1表已分配。padding是因为整个block是要以8bit对齐的,所以如果payload没有满8bit就需要padding,使得整个block以8bit对齐

整个例子:

函数

image-20221127225838654

分配的block(要记得上文说的block的结构)

image-20221127225813866

确定好大小后 block

image-20221127225910181

上面这个例子我们只malloc(6),却整出来16。而每次我们malloc返回的指针是指向我们的payload的,那我们怎么得到block的地址呢?其实也很简单

void *p = malloc(4);
​
uint64_t block_start = ((uint64_t)p -4); //为什么是4?因为meta区大小是32bit 就是4byte
uint32_t block_header_value = *((uint64_t*)block_start;
uint32_t block_size = block_start & 0xfffffffe;
int alloc = block_start & 0x1;
                                
uint64_t next_block_start = block_start + block_size;

mallo实现

malloc原理

隐式空闲链表Implicit Free List

这就是隐式空闲链表,利用数组来管理freelist

uint64_t heap_start_addr;
​
void * malloc(uint32_t size)
{
    uint64_t p = heap_start_addr;
​
    while (1)
    {
        uint32_t p_block_value = *((uint64_t*)p);
        uint32_t p_block_alocated = p_block_value & 0x1;
        uint32_t p_block_size = p_block_value & 0xfffffffe;
        uint32_t p_block_alocated_payload_size = p_block_size -4;
​
        if (p_block_alocated == 0 && p_block_alocated_payload_size >= size)
        {
            uint32_t request_block_size = 4 + size;
            if (request_block_size & 0x7 != 0)
            {
                request_block_size = request_block_size & 0xfffffffe + 0x8;
            }
            
            uint64_t split_block_addr = p + request_block_size;
            uint32_t split_block_value = *(uint64_t*)split_block_addr;
            split_block_value = p_block_size - request_block_size;p_block_value = request_block_size;
            p_block_value &= 0x1;
​
            return (void*)(p+4);
        }
        else
        {
            p += p_block_size;
        }
    }
}

地址用uint64_t ,数用uint_32_t

还有在freelist后有一个大小为0已分配的终止头部(已分配是为了后面不考虑块的合并)

image-20221201232153252

但是现在malloc的实现有问题就是我们现在无法实现free最大化。我们现在只能合并后一块,这是与block的结构决定的

那我们怎么解决呢?这是Knuth(Donald Knuth - Wikipedia)提出了一个聪明又通用的方法叫边界标记。基于这种思想我们决定在block的最后添加一个脚部

image-20221130215036659

这样我们就可以通过脚部得到分配情况(前一块和后一块的分配情况)

image-20221201175101444

也因为有了脚部所以对齐的时候payload 大小要是8的倍数,因为头部和脚部同时出现并且大小加起来刚好是8。

在写malloc函数之前可以写一些辅助函数,如获取大小,分配情况(因为在malloc和free中都要用到)

// Round up to next multiple of n:
// if (x == k * n)
// return x
// else, x = k * n + m and m < n
// return (k + 1) * n
static uint64_t round_up(uint64_t x, uint64_t n)
{
    return n * ((x + n - 1) / n);
} //round_up返回的地址是指向payload的static uint32_t getblocksize(uint64_t header){
    assert(0 < header < heapsize -4);
    uint32_t head_value = *((uint32_t *)header);
    return head_value&0xfffffff8;
}
​
static void set_blocksize(uint64_t header_vaddr, uint32_t blocksize){
    *(uint32_t*)header_vaddr &= 0x7;
    *(uint32_t*)header_vaddr |= blocksize;
}
​
static uint32_t get_allocated(uint64_t header){
    assert(0 < header < heapsize -4);
    uint32_t head_value = *((uint32_t *)header);
    return head_value&0x1;
}
​
static void set_allocated(uint64_t header_vaddr, uint32_t allocated){
    *(uint32_t*)header_vaddr &= 0xfffffff8;
    *(uint32_t*)header_vaddr |= allocated;
}
​
static uint64_t get_payload(uint64_t vaddr)
{
    // vaddr can be:
    // 1. starting address of the block (8 * n + 4)
    // 2. starting address of the payload (8 * m)
    return round_up(vaddr, 8);
}
​
static uint64_t get_blockheader(uint64_t vadder){
    return round_up(vaddr,8) - 4;
}
​
static uint64_t get_nextheader(uint64_t vaddr){
    uint64_t header_addr = get_blockheader(vaddr);
    uint32_t block_size = get_blocksize(header_addr);
    return header_addr + block_size;
}
​
static uint64_t get_prevheader(uint64_t vaddr){
    uint64_t header_addr = get_blockheader(vaddr);
    uint64_t prev_footer_addr = header_addr -4;
    uint32_t prev_block_size = get_blocksize(prev_footer_addr);
    return header_addr - prev_block_size;
}

malloc与free的实现(firs-fit)

malloc与上面的做法倒是没什么不同,只是用了包装的函数,因为新加的foot只是为了free最大化加的。

而free总体上分为4种情况

image-20221201202449514

uint64_t mem_malloc(uint32_t size){
    assert(0 < size < 4096 - 8);
    
    uint32_t request_block_size = round_up(size,8) + 4 + 4;
​
    uint64_t b = heap_start_addr;
    while(b <= heap_end_adder){
        uint32_t b_blocksize = get_blocksize(b);
        uint32_t b_blockallocated = get_allocated(b);
​
        if(b_blockallocated == 0&& b_blocksize >= request_block_size){
            //fit 
            if(b_blocksize > request_block_size){
                //need split
                set_allocated(b,1);
                set_blocksize(b,request_block_size);
​
                uint64_t split_block_addr = b + request_block_size;
                set_allocated(split_block_addr,0);
                set_blocksize(split_block_addr,request_block_size);
​
                return get_payload(b);
            }
            else {
                //no need to split
                set_allocated(b,1);
                return get_payload(b);
            }
        }
        //miss
        else {
            b = get_nextheader(b);
        }
    }
    return 0;
}
​
void mem_free(uint64_t vaddr){
    assert(vaddr &0x7 == 0x0);
​
    uint64_t req_block_addr = get_blockheader(vaddr);
    uint32_t req_block_size = get_blocksize(req_block_addr);
    uint32_t req_block_allocated = get_allocated(req_block_addr);
    assert(req_block_allocated == 0x1);
​
    //to konw the situation of the next block and prev block
    uint64_t next_block_addr = get_nextheader(req_block_addr);
    uint64_t prev_block_addr = get_prevheader(req_block_addr);
​
    uint32_t next_block_size = get_blocksize(next_block_addr);
    uint32_t prev_block_size = get_blocksize(prev_block_addr);
​
    uint32_t next_block_allocated = get_allocated(next_block_addr);
    uint32_t prev_block_allocated = get_allocated(prev_block_addr);
​
    //four situation for free
    if(next_block_allocated == 1 && prev_block_allocated == 1){
        set_allocated(req_block_addr,0);
    }
    else if(next_block_allocated == 0 && prev_block_allocated == 1){
        set_allocated(req_block_addr,0);
        set_blocksize(req_block_addr,req_block_size+next_block_size);
    }
    else if(next_block_allocated == 1 && prev_block_allocated == 0){
        set_allocated(prev_block_addr,0);
        set_blocksize(prev_block_addr,prev_block_size+req_block_size);
    }
    else if(next_block_allocated == 0 && prev_block_allocated == 0){
        set_allocated(prev,0);
        set_blocksize(prev_block_addr,prev_block_size+req_block_size+next_block_size);
    }
}

但是我们在这里乱写也不知道对错,所以我们还要借助malloc lab这个实验。实际上这些代码也只是写一个感觉(其实在我们上面写的malloc中少了一步就是如果在freelist中找不到就要extend_heap,因为这个我不能乱写,其实主要也是不会写,所以我干脆就不写了。还有比如一个block经过split后剩下的block放不下meta区怎么办?按理说是不分割,可是上面的代码没有)

另外在我们现在实现的malloc效率是比较低的,尽管free是o(1),但是malloc是要搜索所有的block是一个o(n)

那我们第一次优化就是让freelist成为一个真正的freelist(因为在之前我们都是把block 数组当初freelist难免有些名不副实),就是malloc遍历时只遍历free的block,就是在所有的block数组中另外有一个freeblock组成的freelist,这样效率能提高100%,因为一个free block周围肯定有一个allocated block(根据free最大原则)

显式空闲链表Explicit Free List

显式空闲链表block,分为free block 和allocated block

image-20221201221944590

我们对block的排序总体上有两种,一是按照LILO的原则,二时按照地址大小,比如从大到小。

我是选择FIFO。

27-dynamic-mem1.pptx (williams.edu))这个ppt写的很完全了

分离链表(Segregated Free Lists)

还有一种Segregated Free Lists(分离链表):在系统中维持多个空闲链表,每个空闲链表内放置某个大小返回内的空闲块

malloc实现(malloc lab)

malloc lab本身

1.获取: CS:APP3e, Bryant and O'Hallaron (cmu.edu)往下翻找到malloc lab,然后点击self-study handout 下载下来解压就行

然后根据writeup写的 Use the command make to generate the driver code and run it with the command ./mdriver -V.

2.解压后make 发现出错

image-20221202163232972

错误原因:系统中的gcc没有安装multilib 库;使用这个库可以在64位的机器上产生32位的程序

解决方法:sudo apt-get install gcc-multilib

3.打开后实现少了trace就是测试文件 可以通过这个链接(CSAPP-Labs/yzf-malloclab-handout at master · Ethan-Yan27/CSAPP-Labs (github.com))下载 (clone) 把trace文件放到自己的malloc lab里

image-20221202163926722

然后在config.h中 把tracedir改成这样

image-20221202164023713

运行 **./mdriver -V**测试一下

image-20221202164126172

要做什么?writeup已经写了

How to Work on the Lab :

Your dynamic storage allocator will consist of the following four functions, which are declared in mm.h and defined in mm.c.

int mm_init(void);
void *mm_malloc(size_t size);
void mm_free(void *ptr); 
void *mm_realloc(void *ptr, size_t size);

就是完成这mm.c中的这4个函数,然后运行/mdriver -V测试成绩

还有一些关于heap的辅助函数(位于memlib.c)就是维护三个变量和一些函数

static char *mem_start_brk;  /* points to first byte of heap */
static char *mem_brk;        /* points to last byte of heap */
static char *mem_max_addr;   /* largest legal heap address */void mem_init(void);               //初始化 heap
void mem_deinit(void);             //将整个heap置为free
void *mem_sbrk(int incr);          //相当于sbrk 返回old mem_brk
void mem_reset_brk(void);          //reset *mem_brk指针
void *mem_heap_lo(void);           //return (void*)*mem_start_brk heap起始地址
void *mem_heap_hi(void);           //return (void*)*mem_brk - 1 heap现在地址
size_t mem_heapsize(void);         //返回mem_brk - mem_start_brk
size_t mem_pagesize(void);         //return the page size of system

image-20221202172435392

另外上面乱写的代码虽然不是完全对的,但是改一下参数还是可以拿来用的

在csapp原书中在mm,c中开头有一些宏来辅助,其实他们和我们写的函数没什么不同,就是把函数变成宏而已

还有malloc lab中地址是用unsigned int * ,我是用uint64_t*

/* 头部/脚部的大小 */
#define WSIZE 4
/* 双字 */
#define DSIZE 8/* 扩展堆时的默认大小 */
#define CHUNKSIZE (1 << 12)
​
#define GET(p) (*(unsigned int *)(p))  //get_blockvalue
#define PUT(p, val) ((*(unsigned int *)(p)) = (val)) //set_blockvalue#define PACK(size, alloc) ((size) | (alloc)) //相当于把我们的set_blocksize和set_allocated合在一起#define GET_SIZE(p) (GET(p) & ~0x7)  //get_blocksize
#define GET_ALLOC(p) (GET(p) & 0x1) //get_allocated
#define HDRP(bp) ((char*)(bp) - WSIZE) //get_blockheader
#define FTRP(bp)    ((char *)(bp)+GET_SIZE(HDRP(bp))-DSIZE)
#define NEXT_BLKP(bp) ((char*)(bp) + GET_SIZE(((char*)(bp) - WSIZE))) //get_nextheader
#define PREV_BLKP(bp) ((char*)(bp) - GET_SIZE(((char*)(bp) - DSIZE))) //get_prevheader

隐式空闲链表+first-fit

init

首先说隐式空闲链表书上实现的了,也就是说代码书上是有的,我们现在是做一个相当于代码完成注释的事情

初始化就是构建出Implicit Free List,而Implicit Free List要求如图

image-20221201232153252

序言块前面是一个双字边界对齐的不使用的填充字,序言块是一个8字节的已分配块,包括header和footer,然后heap总是以一个结尾块结尾,这个结尾块是一个大小为0的分配块只有

所以初始化时我们要构建出这三个特殊块

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));         /* 结尾块 */

完成后,现在我们的表有了这四个块,不过显而易见的是不够,所以我们还要对这个表做extend,借助一个叫extned_heap函数,扩展CHUNKSIZE大小字节,并创建空闲块,这个函数在两件事中被调用:1初始化 2.freelist中没有满足的块

所以我们现在要做两件事,一是在初始化时调用extend_heap函数,二自然是实现extend_heap函数

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);  //因为heap_list总是要指向序言块的第二块

    /* 扩展空闲空间 */
    if(extend_heap(CHUNKSIZE/WSIZE) == NULL)
        return -1;
    return 0;
}

extend_heap函数有它实现上的细节,它分配出一个块接在原来的heap上,将原来的结尾块(其实它本身就是一个header)作为新的块的header,然后在新的块上分出一个结尾块,所以在extend_heap函数中要调用函数进行merge。

extend_heap函数是调用mem_sbrk函数它返回的是指向payload的指针(因为它已经有头部了,就是之前的结束快)

mem_sbrk分配的块应该是与8对齐的,所以要根据奇偶来设置

static void *extend_heap(size_t words){
    char *bp; ///* bp should be pointed to payload
    size_t size;

    size = (words%2) ? (words+1)*WSIZE : words*WSIZE; //根据奇偶对齐
    if((long)(bp=mem_sbrk(size))==(void *)-1) //分配
        return NULL;

    PUT(HDRP(bp),PACK(size,0)); //设置header 其实就是设置old结束块,因为mem_sbrk返回old mem_brk(指向结束块)
    PUT(FTRP(bp),PACK(size,0));
    PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1)); //set new 结束块

    return coalesce(bp); //merge
}

image-20221203205752847

merge(coalesce)

合并其实我们上面已经讨论过了,就是分成四种情况

和上面的代码几乎是一样的,只是用了宏而已

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{
        size += GET_SIZE(HDRP(PREV_BLKP(bp))) + GET_SIZE(FTRP(NEXT_BLKP(bp)));  //增加当前块大小
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));
        bp = PREV_BLKP(bp);
    }
    return bp;
}
malloc

malloc实现:

1.将fit算法抽离出来,做成函数

2.使用了上面写的round_up函数进行对齐(其实也没有)

3.在寻找合适块的时候,除了要看大小是否符合要求,还要看split过后,剩下的块能否放得下block结构(就是是否大于8-一个header+一个footer

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);//书上代码
    //round_up(size,8); 我们自己的对齐函数
    /* 寻找合适的空闲块 */
    if((bp = first_fit(asize)) != NULL){
        place(bp, asize); //split
        return bp;
    }
    /* 找不到则扩展堆 */
    extendsize = MAX(asize, CHUNKSIZE);
    if((bp = extend_heap(extendsize/WSIZE)) == NULL)
        return NULL;
    place(bp, asize); //split
    return bp;
}
first-fit
void *first_fit(size_t asize)
{
    void *bp;
    for(bp = heap_list; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp)){
        if((GET_SIZE(HDRP(bp)) >= asize) && (!GET_ALLOC(HDRP(bp)))){
            return bp; //size符合并且没有分配
        }
    }
    return NULL;
}
split(place)
void place(void *bp, size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp));
​
    /* 判断是否能够分离空闲块 */
    if((csize - asize) >= 2*DSIZE) {  //就是看split过后,剩下的块能否放得下block结构
        PUT(HDRP(bp), PACK(asize, 1));
        PUT(FTRP(bp), PACK(asize, 1));
        bp = NEXT_BLKP(bp);
        PUT(HDRP(bp), PACK(csize - asize, 0));
        PUT(FTRP(bp), PACK(csize - asize, 0));
    }
    /*不用分割*/
    else{
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}
free

书上把free 和merge代码分离了,所以free代码就没什么难度了,就是把header和footer置0

void mm_free(void *ptr)
{
    if(ptr==0)
        return;
    size_t size = GET_SIZE(HDRP(ptr));
​
    PUT(HDRP(ptr), PACK(size, 0));
    PUT(FTRP(ptr), PACK(size, 0));
    coalesce(ptr);
}
realloc

指针名=(数据类型*)realloc(要改变内存大小的指针名,新的大小)。

就照着writeup写的要就行

image-20221202223929808

image-20221202224000019

void *mm_realloc(void *ptr, size_t size)
{
    size_t oldsize;
    void *newptr;

    /* If size == 0 then this is just free, and we return NULL. */
    if(size == 0) {
	mm_free(ptr);
	return 0;
    }

    /* If oldptr is NULL, then this is just malloc. */
    if(ptr == NULL) {
	return mm_malloc(size);
    }

    newptr = mm_malloc(size);

    /* If realloc() fails the original block is left untouched  */
    if(!newptr) {
	return 0;
    }

    /* Copy the old data. */ 
    oldsize = GET_SIZE(HDRP(ptr));
    if(size < oldsize) oldsize = size;
    memcpy(newptr, ptr, oldsize); //不管大小就把按最小的大小复制过来

    /* Free the old block. */
    mm_free(ptr); //再释放掉原来的块

    return newptr;
}
结果

image-20221202230922202

分析:

我们malloc lab与我们上面写的代码有什么不同吗?

1.malloc lab所有函数都统一参数是指向payload的指针,再通过宏进行操作。而我们的代码则利用了round_up(x,8)进行转换,就是无论是什么地址放进round函数(参数也只会有header addr 和 payload地址),出来都是指向payload的指针。为什么?

image-20221203000702766

所以所有的payload区域都8对齐的,而且round只会返回指向payload的指针(因为参数只有header addr 和 payload地址)

2.其实我们上面的代码挺完善了,我们加了什么呢?

1.使用mem_sbrk实现的extend_heap,我们原来的代码就没有block的扩展和结尾块的合并

2.再split是加了检测,检测split后的block能不能放下block结构

显式空闲链表+fifo

image-20221203134109330

来源:(27-dynamic-mem1.pptx (williams.edu))写的真的很好,靠这个pdf写Explicit Free List完全够了。

还有一点很重要就是就下来所以bp指针再上面是指向paypoad,在下面的函数是指向prev指针(因为prev和next在块的结构里就算在payload里面,所以其实是一样的)

要加一些对链表设置的宏

#define PREV_LINKNODE_RP(bp) ((char*)(bp))       //块的prev
#define NEXT_LINKNODE_RP(bp) ((char*)(bp)+WSIZE) //块的next
init

还是与Implicit Free List一样构建Explicit Free List就行

显式相比于隐式就是block加了prev和next指针,那么显式list初始化时就要为链表服务,init出一个链表头root(有一个prev指针和next指针)

所以要sbrk6个块了,再加上对root的初始化。(可能与上面那个图不一样,但在那个pdf中关于free时都有root的出现)

image-20221203144617447

int mm_init(void)
{
    if((heap_listp = mem_sbrk(6*WSIZE))==(void *)-1) return -1; //知道为什么是6吗
​
    PUT(heap_listp,0);
    PUT(heap_listp+(1*WSIZE),0);   //prev
    PUT(heap_listp+(2*WSIZE),0);   //next 换言之init root
    PUT(heap_listp+(3*WSIZE),PACK(DSIZE,1)); /* 填充序言块 */
    PUT(heap_listp+(4*WSIZE),PACK(DSIZE,1)); /* 填充序言块 */
    PUT(heap_listp+(5*WSIZE),PACK(0,1));   /* 填充结尾块 */
    root = heap_listp + (1*WSIZE);     //root
​
    heap_listp += (4*WSIZE);  //因为heap_list总是要指向序言块的第二块
​
    /* 扩展空闲空间 */
​
    if((extend_heap(CHUNKSIZE/DSIZE))==NULL) return -1;
    
    return 0;
}

而extend_heap函数与隐式也有两点不同 一是大小有了变化现在最小是16byte(header 4 +next 4+prev 4 +footer 4)

二是扩展后的内存多了next prev 的初始化

static void *extend_heap(size_t words)
{
    char *bp;
    size_t size;
​
    size = (words % 2) ? (words+1) * DSIZE : words * DSIZE;
​
    if((long)(bp = mem_sbrk(size))==(void *)-1)
        return NULL;
​
    PUT(HDRP(bp),PACK(size,0)); //设置header 其实就是设置old结束块,因为mem_sbrk返回old mem_brk(指向结束块)
    PUT(FTRP(bp),PACK(size,0)); 
    PUT(NEXT_LINKNODE_RP(bp),0); //设置next
    PUT(PREV_LINKNODE_RP(bp),0);
​
    PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1)); //set new 结束块
​
    return coalesce(bp);
}

image-20221203210302455

merge(coalesce)

合并就是分成四种情况

1.前后都不空

image-20221203210554519

做法:把这个块直接插入链表头部后面(头插法)

2.前不空后空

image-20221203210721635

做法:把后面的块先从链表取下,再进行合并最后头插法

3.前空后不空

image-20221203211022640

做法:把前面的块先从链表取下,再进行合并最后头插法

4.前后都空

image-20221203211104134

做法:把前面和后面的块先从链表取下,再进行合并最后头插法(这些图都是来自上面提到的pdf)

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));
​
    /*coalesce the block and change the point*/
    /* 前后都不空 */
    if(prev_alloc && next_alloc)
    {
        //什么也不做,待会直接返回bp
    }
    /* 前不空后空 */
    else if(prev_alloc && !next_alloc)
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        fix_linklist(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)));
        fix_linklist(PREV_BLKP(bp)); /*前面的块从链表取下*/
        PUT(FTRP(bp),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp); 
    }
    /* 前后都空 */
    else
    {
        size +=GET_SIZE(FTRP(NEXT_BLKP(bp)))+ GET_SIZE(HDRP(PREV_BLKP(bp)));
        fix_linklist(PREV_BLKP(bp)); /*前面的块从链表取下*/
        fix_linklist(NEXT_BLKP(bp));/*后面的块从链表取下*/
        PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp);
    }
    insert_to_Emptylist(bp);  //头插法
    return bp;
}
malloc

由于fit被设计成接口,所以malloc与上面的代码一样,所以就是重写first-fit和place函数

image-20221203220638164

找到block是first-fit的工作

先从链表把找到的块取下,split然后再重新接上链表(调用insert_to_Emptylist函数)是place函数的工作

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));
    }
    //asize = round_up(size,8);
    if((bp = first_fit(asize))!= NULL)
    {
        place(bp,asize);
        return bp;
    }
​
    /*apply new block*/
    extendsize = MAX(asize,CHUNKSIZE);
    if((bp = extend_heap(extendsize/DSIZE))==NULL)
    {
        return NULL;
    }
    place(bp,asize);
    return bp;
}
first-fit
static void *fist_fit(size_t size)
{
    char *temp = GET(root);
    while(temp != NULL)
    {
        if(GET_SIZE(HDRP(temp))>=asize) return temp; //从首节点开始找寻
        temp = GET(NEXT_LINKNODE_RP(temp)); //temp = temp->next
    }
    return NULL;
}
split(place)
static void place(void *bp,size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp));
    fix_linklist(bp);/*先从链表把找到的块取下*/ 
    if((csize-asize)>=(2*DSIZE))
    {
        PUT(HDRP(bp),PACK(asize,1));
        PUT(FTRP(bp),PACK(asize,1));
        bp = NEXT_BLKP(bp); //split后的块
​
        PUT(HDRP(bp),PACK(csize-asize,0));
        PUT(FTRP(bp),PACK(csize-asize,0));
        PUT(NEXT_LINKNODE_RP(bp),0);  
        PUT(PREV_LINKNODE_RP(bp),0);
        coalesce(bp); /*重新接上链表*/
    }
    else
    {
        PUT(HDRP(bp),PACK(csize,1));
        PUT(FTRP(bp),PACK(csize,1)); /*不切割直接取下来就行*/
    }
}
fix_linklist(从链表取block)

p作为我们要取的block

p->prev->next = p->next

p->next->prev = p->prev

inline void fix_linklist(char *p)
{
    char *prevp = GET(PREV_LINKNODE_RP(p));
    char *nextp = GET(NEXT_LINKNODE_RP(p));
    if(prevp == NULL)
    {
        if(nextp != NULL)PUT(PREV_LINKNODE_RP(nextp),0);
        PUT(root,nextp);
    }
    else
    {
        if(nextp != NULL)PUT(PREV_LINKNODE_RP(nextp),prevp);
        PUT(NEXT_LINKNODE_RP(prevp),nextp);
    }
    PUT(NEXT_LINKNODE_RP(p),0);
    PUT(PREV_LINKNODE_RP(p),0);
}
insert_to_Emptylist(头插法)
inline void insert_to_Emptylist(char *p)
{
    char *nextp = GET(root);
    if(nextp != NULL)
        PUT(PREV_LINKNODE_RP(nextp),p);
​
    PUT(NEXT_LINKNODE_RP(p),nextp); //root
    PUT(root,p);
}
free

free 和merge代码分离了,所以free代码就没什么难度了,就是把header和footer置0

void mm_free(void *bp)
{
    if(bp == 0)
        return;
​
    size_t size = GET_SIZE(HDRP(bp));
​
    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    PUT(NEXT_LINKNODE_RP(bp),0);
    PUT(PREV_LINKNODE_RP(bp),0);
    
    coalesce(bp);
}
realloc

跟上面没有区别,因为realloc就是malloc的包装函数

void *mm_realloc(void *ptr, size_t size)
{
    size_t oldsize;
    void *newptr;
​
    /* If size == 0 then this is just free, and we return NULL. */
    if(size == 0)
    {
        mm_free(ptr);
        return 0;
    }
​
    /* If oldptr is NULL, then this is just malloc. */
    if(ptr == NULL)
    {
        return mm_malloc(size);
    }
    oldsize = GET_SIZE(HDRP(ptr));
​
    newptr = mm_malloc(size);
​
    /* If realloc() fails the original block is left untouched  */
    if(!newptr)
    {
        return 0;
    }
​
    /* Copy the old data. */
    oldsize = GET_SIZE(HDRP(ptr));
    if(size < oldsize) oldsize = size;
    memcpy(newptr, ptr, oldsize);
​
    /* Free the old block. */
    mm_free(ptr);
​
    return newptr;
}
结果

image-20221203235858600

分析:

1.与上面有什么不同吗?

找寻blcok是遍寻链表,在malloc 和free是多了从链表取节点和最后重新挂上的步骤

2.那个pdf真的很有用

分离链表(Segregated Free Lists)

其实分离链表与显式没有什么大的区别,就是多维护几个链表而已,而多几个链表也意味着多几个root

而block结构和显式是一样的

#define GET_HEAD(num) (char *)(long)((heap_listp + WSIZE * num)) //通过num找到链表的root
init

多的root在init时就要体现出来,所以我把inti分成两部分:第一部分还是照旧分配4个块 ,第二部分是进行root的分配

并且按照与显式的布局一样把root放在首的后面,序言块的前面.root的数量用root_num表示(自己想要多少个链表就要多少个)。并且root的结构也变了,现在root只占4字节(因为只要有next就行)

int mm_init(void)
{
    if((heap_listp = mem_sbrk((4+root_num)*WSIZE)) == (void *)-1)
        return -1;
    /* 初始化root*/
    for(int i = 0; i < root_num; i++){
        PUT(heap_listp + i*WSIZE, NULL);
    }
 
    PUT(heap_listp + root_num * WSIZE, 0);
    /* 
     * 序言块和结尾块均设置为已分配, 方便考虑边界情况
     */
    PUT(heap_listp + ((1 + root_num )*WSIZE), PACK(DSIZE, 1));     /* 填充序言块 */
    PUT(heap_listp + ((2 + root_num )*WSIZE), PACK(DSIZE, 1));     /* 填充序言块 */
    PUT(heap_listp + ((3 + root_num )*WSIZE), PACK(0, 1));         /* 结尾块 */
​
​
    /* 扩展空闲空间 */
    if(extend_heap(CHUNKSIZE/WSIZE) == NULL)
        return -1;
    return 0;
}

image-20221204143842546

void *extend_heap(size_t words)
{
    /* bp总是指向有效载荷(prev) */
    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)); //设置header 其实就是设置old结束块,因为mem_sbrk返回old mem_brk(指向结束块)
    PUT(FTRP(bp),PACK(size,0)); 
    PUT(NEXT_LINKNODE_RP(bp),0); //设置next
    PUT(PREV_LINKNODE_RP(bp),0);
​
    PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1)); //set new 结束块
​
    /* 判断相邻块是否是空闲块, 进行合并 */
    return coalesce(bp);
}
merge(coalesce)

唯一有变化的就是insert插入链表有变化(但把任务交付给了insert_to_list函数)

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));
​
    /*coalesce the block and change the point*/
    /* 前后都不空 */
    if(prev_alloc && next_alloc)
    {
        //什么也不做,待会直接返回bp
    }
    /* 前不空后空 */
    else if(prev_alloc && !next_alloc)
    {
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        fix_linklist(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)));
        fix_linklist(PREV_BLKP(bp)); /*前面的块从链表取下*/
        PUT(FTRP(bp),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp); 
    }
    /* 前后都空 */
    else
    {
        size +=GET_SIZE(FTRP(NEXT_BLKP(bp)))+ GET_SIZE(HDRP(PREV_BLKP(bp)));
        fix_linklist(PREV_BLKP(bp)); /*前面的块从链表取下*/
        fix_linklist(NEXT_BLKP(bp));/*后面的块从链表取下*/
        PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
        PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
        bp = PREV_BLKP(bp);
    }
    insert_to_list(bp);  //头插法但要找到自己的root(通过GET_HEAD宏)
    return bp;
}
search

通过大小找自己的root num

int search(size_t size)
{
    int i;
    for(i = 4; i <=root_num; i++){
        if(size <= (1 << i)) //alige to 8
            return i-4;
    }
    return i-4;
}
insert_to_list

通过search找到root num,再同过GET_HEAD宏找到root,再进行头插法

跟显式的inser_to_Emptylist只有找root一个改变

inline void insert_to_list(char *p)
{
    int num = search(GET_SIZE(HDRP(p)));
    char *root = GET_HEAD(num);
    char *nextp = GET(root);
    if(nextp != NULL)
        PUT(PREV_LINKNODE_RP(nextp),p);
​
    PUT(NEXT_LINKNODE_RP(p),nextp); //root
    PUT(root,p);
}
​
fix_linklist(从链表取block)

和显式还是只多了一步找root

inline void fix_linklist(char *p)
{
    size_t size = GET_SIZE(HDRP(p));
    /* 根据块大小找到头节点位置 */
    int num = search(size);
    char *root = GET_HEAD(num);
    char *prevp = GET(PREV_LINKNODE_RP(p));
    char *nextp = GET(NEXT_LINKNODE_RP(p));
    if(prevp == NULL)
    {
        if(nextp != NULL)PUT(PREV_LINKNODE_RP(nextp),0);
        PUT(root,nextp);
    }
    else
    {
        if(nextp != NULL)PUT(PREV_LINKNODE_RP(nextp),prevp);
        PUT(NEXT_LINKNODE_RP(prevp),nextp);
    }
    PUT(NEXT_LINKNODE_RP(p),0);
    PUT(PREV_LINKNODE_RP(p),0);
}
malloc

由于fit被设计成接口,所以malloc与上面的代码一样,所以就是重写first-fit和place函数

在不同的链表找到block是first-fit的工作

先从链表把找到的块取下,split然后再重新接上链表(调用insert_to_list函数)是place函数的工作

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 = first_fit(asize))!= NULL)
    {
        place(bp,asize);
        return bp;
    }
​
    /*apply new block*/
    extendsize = MAX(asize,CHUNKSIZE);
    if((bp = extend_heap(extendsize/DSIZE))==NULL)
    {
        return NULL;
    }
    place(bp,asize);
    return bp;
}
first-fit
  • 先从对应的大小类的空闲链表中查找
  • 如果找不到,则到下一个更大的大小类查找
  • 如果都找不到,则扩展堆
  • 相比于显式,只是多了找root和换root两步
static void *find_fit(size_t asize)
{
    int num = search(asize);
    /* 如果找不到合适的块,那么就搜索下一个更大的大小类 */
    while(num < root_num) {
        char *temp = GET(GET_HEAD(num));
        while(temp != NULL){
            if(GET_SIZE(HDRP(temp))>=asize) return temp; //从首节点开始找寻
            temp = GET(NEXT_LINKNODE_RP(temp)); //temp = temp->next
        }
        /* 找不到则进入下一个大小类 */
        num++;
    }
    return NULL;
}
split(place)

place函数与上面没什么不同,因为有关链表的操作都是调用函数,只是函数会有不同

static void place(void *bp,size_t asize)
{
    size_t csize = GET_SIZE(HDRP(bp));
    fix_linklist(bp);/*先从链表把找到的块取下*/ 
    if((csize-asize)>=(2*DSIZE))
    {
        PUT(HDRP(bp),PACK(asize,1));
        PUT(FTRP(bp),PACK(asize,1));
        bp = NEXT_BLKP(bp); //split后的块
​
        PUT(HDRP(bp),PACK(csize-asize,0));
        PUT(FTRP(bp),PACK(csize-asize,0));
        PUT(NEXT_LINKNODE_RP(bp),0);  
        PUT(PREV_LINKNODE_RP(bp),0);
        coalesce(bp); /*重新接上链表*/
    }
    else
    {
        PUT(HDRP(bp),PACK(csize,1));
        PUT(FTRP(bp),PACK(csize,1)); /*不切割直接取下来就行*/
    }
}
free

free和上面代码是一样的,因为涉及到链表的代码都交给coalesce函数了

void mm_free(void *bp)
{
​
    if(bp == 0)
        return;
    size_t size = GET_SIZE(HDRP(bp));
​
    PUT(HDRP(bp), PACK(size, 0));
    PUT(FTRP(bp), PACK(size, 0));
    PUT(NEXT_LINKNODE_RP(bp),NULL);
    PUT(PREV_LINKNODE_RP(bp),NULL);
    coalesce(bp);
}
realloc

跟上面没有区别,因为realloc就是malloc的包装函数,而且我也不打算优化

void *mm_realloc(void *ptr, size_t size)
{
    size_t oldsize;
    void *newptr;
​
    /* If size == 0 then this is just free, and we return NULL. */
    if(size == 0)
    {
        mm_free(ptr);
        return 0;
    }
​
    /* If oldptr is NULL, then this is just malloc. */
    if(ptr == NULL)
    {
        return mm_malloc(size);
    }
    oldsize = GET_SIZE(HDRP(ptr));
​
    newptr = mm_malloc(size);
​
    /* If realloc() fails the original block is left untouched  */
    if(!newptr)
    {
        return 0;
    }
​
    /* Copy the old data. */
    oldsize = GET_SIZE(HDRP(ptr));
    if(size < oldsize) oldsize = size;
    memcpy(newptr, ptr, oldsize);
​
    /* Free the old block. */
    mm_free(ptr);
​
    return newptr;
}
    
结果

image-20221204185632875

分析:

相比显式就是多管理几个链表,所以对应的函数,就多了关于找root换root的步骤

另外在做的时候有错误是关于寻找next的时候,关于地址直接解引用导致segment fult,找死我了

总结

1.我是想把我从0开始到实现的全过程记录下来,可能会显得繁琐导致可能没什么人看。(也很正常,毕竟我实际在想的时候,思维就会转圈,不是说思维是螺旋上升的吗),过程中很多segment fault 但是我的大部分是宏的问题,后来我的宏就是照书硬抄,问题就少了很多。

2.我还是想把我代码的一遍一遍迭代的过程说清楚,说明他们之间有什么不同,具体是怎么优化的(其实也没有很具体,毕竟我数学很不好),而且如果是慢慢迭代上来的就会发现没什么难的(大概),显式比隐式多了链表,分离比显式多了几个链表

3.红黑树我就没打算写了(我这个代码水平有限,时间也有限),但是如果你对它很感兴趣可以去看b站的一个up(我觉得他超强的)(第三种实现:红黑树哔哩哔哩bilibili

4.本来的重点就是关注malloc 和free的实现,所以对realloc就没什么优化

5.最近我突然听到曹格的格格blue专辑,我天超好听。

参考:

1.最早是看到a malloc tutorial(wiki-prog.infoprepa.epita.fr/images/0/04…)这个pdf和blog(danluu.com/malloc-tuto…)开始有想法的

2.上面提到的up yaaangminyaaangmin的个人空间哔哩哔哩bilibili

3.是参考了许多blog对malloc lab的实现(太多了我就不列举了)

4.书上的源代码csapp.cs.cmu.edu/3e/ics3/cod…