内存管理器(三)使用边界标识法实现内存分配器

使用边界标识法实现简单分配器

前言

前一篇我们已经了解了边界标识算法和数据结构,其实边界标识法还是比较复杂的,它的难点在于对C的使用的淋漓尽致,以及复杂的逻辑关系。所以我们还需要多思考,多体会才能领悟个中精髓,其实我昨天在学习那个小例子的时候由一瞬间感觉如果用C++实现可能更方便,所以再此决定等这几篇完成,就使用C++实现一个小小的实例。
言归正传,我们今天需要看一个用C使用边界标识法实现的简单分配器,这个分配器主要是首先重内存申请一大片内存,然后根据程序的调用来分配空间的有点像实现一个malloc/free 函数。

学习目标

1.通过这个实例,加深边界标识法的理解。
2.对这个实例从效率,多线程等角度进行测试。
3.为后边伙伴算法打基础。

PS:个人还是觉得大学学习两件事:一个看天,一个看自己。
1.看天是根据自己的性格特点,兴趣爱好学习。
2.看自己处了努力学习外,还有练就总结的能力,大学的课废话太多。(又跑偏了)

__START

块的结构

malloc

如图所示
基本上一个堆块的大小就是这个样子的,并不陌生呢,我前边已经介绍过了,基本都是由头部,内存块,尾部组成的。

这里需要说明的是:
标志位已经不再是一个int了,而是3位,因为内存对齐的关系我们需要按照4/8字节对齐,假设这里按照4字节对齐,那么就是0~31个位,我们释放低3位来作为状态的标识。

举个例子:
假设我们有一个已经分配的块大小为24(0x18) 字节。那么它的头部是
0x00000018 | 0x1 = 0x00000019

再加设我们有一个没有分配的块大小为40字节。那么它的头部是
0x00000028 | 0x00 = 0x00000028

在有效载荷后边由一块填充块,这个块是不使用的,因为现在用不上不代表以后都用不上,所以我们要为可持续发展做长远的考虑。

安置已分配的块

放置策略

首次适配 :每一次都从头开始给它匹配一个可以使用的块。
下次适配 :每次从上次分配的块后+1 开始匹配一个块。
最佳适配 :每次遍历整个链表,然后找到一个我们认为合适的块给它。
这些和我们之前搞的东西很像。

申请额外的空间

当我们的预申请的对空间不够用时,我们就可以调用sbrk 函数来提升堆的大小。

基本思路纵向导图

malloc
我们使用系统调用mmap( )函数从内存中请求一片100MB 的大小的空间当作我们的对空间,初始化一个序言块这个主要是为了便与我们后边的边界计算。

分步解析内存管理系统

0.我们的变量和一些宏处理
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<sys/mman.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>

#define MAX_HEAP  (100 *(1 << 20))//100MB 大小

#define MINSIZE 2

#define WSIZE 4           //一个字的大小4KB
#define DSIZE 8           //双字大小8KB

#define CHUNKSIZE (1<<12)  //初始空闲堆的大小和默认的大小  4MB

#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)))  //返回上一个指针


static char *heap_listp = 0;
static char *heap;
static char *mem_brk;
static char *mem_max_addr;


static char *flist_free = NULL;   //指向释放的链表
static char *heap_tailp = NULL;   //指向最后一块
static int  count  = 0;           //指示链表中块的数目
1.内存空间申请
void mem_init(void){           
    //从内存里拿出一个100MB 的空间来作为我们的堆空间
    int dev_zero = open("/dev/zero",O_RDWR);
    heap = mmap((void *)0x800000000,MAX_HEAP,PROT_WRITE,
                  MAP_PRIVATE,dev_zero,0);
    mem_max_addr = heap + MAX_HEAP;
    mem_brk = heap;
}

效果如图:
init_heap
成功从内存条申请了100MB 的空间

2.初始化我们的堆空间,建立序言块。
int mm_init(void){    //初始化序言块
    if((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)  
     // brk :0x800000010
            return -1;
    init_free_list();
    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 += (2*WSIZE);                                //heap_listp::0x800000008
    count++; 
 //链表中现在只有一个序言块
 //保持对齐并且,根据需求请求更多的堆存储器,初始化堆空间4KB
        return -1;
    return 0;
}

效果如图:
mem
现在我们可以看到各个位置已经初始化完毕。

4.堆扩展函数以及新增空闲块函数
static void *extend_heap(size_t words){
//用一个新的空块扩展堆
    char *bp;
    size_t size;

    size = (words % 2)?(words + 1)*WSIZE : words *WSIZE;
    //保持和2的倍数对齐,内存对齐
    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 bp;

}

void *mem_sbrk(int incr){       
//申请额外的堆空间就是malloc 的分配,分配主要的工作函数
    char *old_brk = mem_brk;
    if((incr < 0) || ((mem_brk + incr) > mem_max_addr)){
      //超过大小或者超过我们的堆空间大小都会报错
        printf("out of memory\n");   
      //brk 来返回堆的尾部地址
        return (void *)-1;
    }
    mem_brk += incr;
    return (void *)old_brk;
}
5.分配函数malloc( )
void *malloc(size_t size){
    size_t asize;   //建议块的大小,根据size 且需要对齐
    size_t extendsize;
    char *bp;
    if(size == 0)
        return NULL;
    if(size <= DSIZE)
        asize = 2*DSIZE;
    else
        asize = DSIZE*((size + (DSIZE) + (DSIZE-1)) / DSIZE);
         //设置最小的块为16 其中留了8字节为头部和脚部
    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;

}

static void place(void *bp,size_t asize){
     //分配函数,根据需求分配相应的块,并把它放置到,我们的堆链后
    size_t csize = GET_SIZE(HDRP(bp));     
       //csize 得到这个块的大小
    if((csize - asize) >= (2*DSIZE)){      
        //如果分配减去需求大于最小块的大小,进行分割
        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(HDRP(bp),PACK(csize,1));
    }


}
6.释放函数free 以及合并函数coalesce
void free(void *bp){               
//回收我们的内存,有用有还,再借不难
    size_t size = GET_SIZE(HDRP(bp));
    PUT(HDRP(bp),PACK(size,0));
    PUT(FTRP(bp),PACK(size,0));
    coalesce(bp);
}


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{   
  //前后都是空闲,直接全部合并  
    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);
    }

    return bp;
}
匹配算法这里主要是首次适配
static void *find_fit(size_t asize){  //首次适配
    char *bp;
    int   len = 0;
    for(bp = heap_listp;GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp),len++){
    if(!GET_ALLOC(HDRP(bp))  && (asize <= GET_SIZE(HDRP(bp)))){
      //寻找第一个可以匹配的块
     return  (void *)bp;
        }      
    }
    return NULL;
}

这里都是简单的实现,我们也可以使用下次适配,和最佳适配。
下次适配很简单:
加上:

heap_listp = bp;  //每次让它指向最后分配的位置就好,下次从这里匹配

最佳适配:这里就是改一下for语句就好,读者可以自行实现。但是说明一点,最佳匹配在边界标识法体现的并不强烈,可以说它其实是伙伴算法的特性。

8.其他测试函数
static void print(){  //测试函数
    printf("the heap :%p\n heap_brk:%p \n heap_max_addr:%p\n",heap,mem_brk,mem_max_addr);
    printf("the heap_listp:%p    the flist_free is %p \n",heap_listp,flist_free);
}

static void print_blcok(void *bp){
    printf("the heap_prev:%p   heap_next is %p \n",PREV_BLKP(bp),NEXT_BLKP(bp));
}

static void print_show(void){
    char *temp;
    int a = 10;
    for(temp = heap_listp; temp != mem_brk;temp = NEXT_BLKP(temp),a--)
    { 
        printf("the start is %p \n ",temp);
        printf("the flag is %d\n",GET_ALLOC(temp));
    }
}

测试与运行

1.测试

int main(){
    char *p;
    char *q;
    char *t;
    mem_init();
    mm_init();
    print();
    p = (char *)malloc(sizeof(char)*10);
    q = (char *)malloc(sizeof(char) *10);
    t = (char *)malloc(sizeof(char) *10);
    print_show();
    free(p);
    free(q);
    free(t);
    print();
    print_show();

}

运行结果:
结果

可以看到,第一个序言块在分配前后都是1。
计算他们的大小:24 这是对的,首先我们需求10 但是按照8字节对齐,给他们分配16字节大小,然后再加上头和尾各4字节。所以一共:
16 + 8 = 24 字节
并且我们发现,在释放后,我们的程序已经他们合并了。

2.多线程测试

void *one(void *arg){

    while(1){
        char *p;
        p = (char *)malloc(sizeof(char)*10);
        free(p);
        printf("i am the one thread\n");
        sleep(1);
    }
}

void *two(void *arg){

    while(1){
        char *q;
        q = (char *)malloc(sizeof(char)*10);
        free(q);
        printf("i am the two thread\n");
        sleep(1);
    }
}

int main(){
    mem_init();
    mm_init();
    pthread_t thid1;
    pthread_t thid2;
    int err;
    err = pthread_create(&thid1,NULL,one,NULL);
    err = pthread_create(&thid2,NULL,two,NULL);

    while(1){
        sleep(1);
    }

}

运行结果如图所示:
多线程测试
虽然看到这里在多线程的时候可以执行,但是确实线程极不安全的,最简单的来说多个线程一个锁都没有用到,这怎么可能支持多线程,所以不能支持多线程,后续我们还会对这个分配器进行改造,甚至是重构!

my_malloc VS sys_malloc 和系统的malloc函数对比

int main(){
     //根据多次分配释放来看看效率
    int i = 10000000;
    while(i > 0){
        char *p;
        p = (char *)malloc(sizeof(char)*10);
        free(p);
        i--;
    }
    return 0;
}

效果如图
测试结果
我被系统完爆。。。.。。.。.。。。。.。
当然这只是一个简单的实现,以后再做优化。

预告:

下来:
内核内存,伙伴算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值