您的位置:首页 > 编程语言 > C语言/C++

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

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;
}


效果如图



我被系统完爆。。。.。。.。.。。。。.。

当然这只是一个简单的实现,以后再做优化。

预告:

下来:

内核内存,伙伴算法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息