内存管理器(三)使用边界标识法实现内存分配器
2015-10-16 17:29
561 查看
使用边界标识法实现简单分配器
前言
前一篇我们已经了解了边界标识算法和数据结构,其实边界标识法还是比较复杂的,它的难点在于对C的使用的淋漓尽致,以及复杂的逻辑关系。所以我们还需要多思考,多体会才能领悟个中精髓,其实我昨天在学习那个小例子的时候由一瞬间感觉如果用C++实现可能更方便,所以再此决定等这几篇完成,就使用C++实现一个小小的实例。言归正传,我们今天需要看一个用C使用边界标识法实现的简单分配器,这个分配器主要是首先重内存申请一大片内存,然后根据程序的调用来分配空间的有点像实现一个malloc/free 函数。
学习目标
1.通过这个实例,加深边界标识法的理解。2.对这个实例从效率,多线程等角度进行测试。
3.为后边伙伴算法打基础。
PS:个人还是觉得大学学习两件事:一个看天,一个看自己。
1.看天是根据自己的性格特点,兴趣爱好学习。
2.看自己处了努力学习外,还有练就总结的能力,大学的课废话太多。(又跑偏了)
__START
块的结构
如图所示
基本上一个堆块的大小就是这个样子的,并不陌生呢,我前边已经介绍过了,基本都是由头部,内存块,尾部组成的。
这里需要说明的是:
标志位已经不再是一个int了,而是3位,因为内存对齐的关系我们需要按照4/8字节对齐,假设这里按照4字节对齐,那么就是0~31个位,我们释放低3位来作为状态的标识。
举个例子:
假设我们有一个已经分配的块大小为24(0x18) 字节。那么它的头部是
0x00000018 | 0x1 = 0x00000019
再加设我们有一个没有分配的块大小为40字节。那么它的头部是
0x00000028 | 0x00 = 0x00000028
在有效载荷后边由一块填充块,这个块是不使用的,因为现在用不上不代表以后都用不上,所以我们要为可持续发展做长远的考虑。
安置已分配的块
放置策略
首次适配 :每一次都从头开始给它匹配一个可以使用的块。下次适配 :每次从上次分配的块后+1 开始匹配一个块。
最佳适配 :每次遍历整个链表,然后找到一个我们认为合适的块给它。
这些和我们之前搞的东西很像。
申请额外的空间
当我们的预申请的对空间不够用时,我们就可以调用sbrk 函数来提升堆的大小。基本思路纵向导图
我们使用系统调用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; }
效果如图:
成功从内存条申请了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; }
效果如图:
现在我们可以看到各个位置已经初始化完毕。
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; }
效果如图
我被系统完爆。。。.。。.。.。。。。.。
当然这只是一个简单的实现,以后再做优化。
预告:
下来:内核内存,伙伴算法。
相关文章推荐
- 应用领航:盘点那些年我们一起追过的OS
- 无奇不有!盘点各国自己开发的操作系统
- Lua和C语言的交互详解
- Lua的内存管理浅析
- 可自定义oem的萝卜家园 Ghost XP 新春装机版 V200801 下载
- C#实现判断操作系统是否为Win8以上版本
- 关于C语言中参数的传值问题
- 简要对比C语言中三个用于退出进程的函数
- 深入C++中API的问题详解
- 基于C语言string函数的详解
- C语言中fchdir()函数和rewinddir()函数的使用详解
- C语言内存对齐实例详解
- 使用C语言判断英文字符大小写的方法
- c语言实现的带通配符匹配算法
- C语言实现顺序表基本操作汇总
- C语言中计算正弦的相关函数总结
- 使用C语言详解霍夫曼树数据结构
- 探讨C语言的那些小秘密之断言
- C语言实现BMP转换JPG的方法
- 深入探讨C语言中局部变量与全局变量在内存中的存放位置