malloc的基本实现

来源:互联网 发布:双十一网络瘫痪 编辑:程序博客网 时间:2024/06/01 09:05

 

一直对malloc的实现不是很懂,K&R的C程序设计语言的8.7节讲了一个malloc的基本实现,这篇文章主要把自己的理解记录一下。

malloc是在堆里申请空间的,每一次申请的空间叫做内存块,每个内存块包括头部和数据区,空闲的内存块通过一个循环链表组织在一起,内存块的地址是从低到高排列的,这是由堆的地址是从低到高生长决定的,链表有一个表头,表头不包含数据区。

  clip_image001[4] 

 

 

 

为了简化块的对齐,每个内存块的大小都是头部的整数倍,头部通过一个联合体来实现,包括指向下一个块的指针和当前块的长度:

typedef long Align;/*for alignment to long boundary*/union header {    struct {        union header *ptr; /*next block if on free list*/        unsigned size; /*size of this block*/    } s;    Align x;  //强制块的对齐};typedef union header Header;

当第一次使用malloc时,首先让base指向自己



malloc申请空间时遍历每一个空闲内存块,这里分为3种情况:

1)      当申请的空间刚好为块的长度时,返回该块的数据区地址,并把块从链表中移除

2)      当申请的空间小于块的长度时,从尾部分裂一个数据块,并将数据区地址返回给用户

3)     当遍历完整个链表后,不存在内存块满足申请时,那么调用morecore()向操作系统申请一个更大的内存块插入到内存中,实现代码如下:

static Header base;  //链表表头static Header *freep = NULL; //空闲链表的初始指针 void *malloc(unsigned nbytes){    Header *p, *prevp;    unsigned nunits;    nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1;//申请的空间为Header长度的的整数倍    if((prevp = freep) == NULL) { /* no free list */        base.s.ptr = freep = prevp = &base;        base.s.size = 0;}//从freep的下一个内存块开始遍历链表    for(p = prevp->s.ptr; ;prevp = p, p= p->s.ptr) {        if(p->s.size >= nunits) { /* big enough */            if (p->s.size == nunits)  /* exactly */                prevp->s.ptr = p->s.ptr;//将这个块从链表移除            else {                //将这个块的尾部分割出去                p->s.size -= nunits;                p += p->s.size;                p->s.size = nunits;            }            freep = prevp;            return (void*)(p+1);//返回数据区        }        if (p== freep) /* wrapped around free list */            if ((p = morecore(nunits)) == NULL)//向操作系统申请空间                return NULL; /* none left */    }}

为了讲解morecore的实现,首先简单介绍一下操作系统的内存排布,这个主要摘自以下文章

http://blog.codinglabs.org/articles/a-malloc-tutorial.html

 

“根据Linux内核相关文档描述,Linux64位操作系统仅使用低47位,高17位做扩展(只能是全0或全1)。所以,实际用到的地址为空间为0x0000000000000000 ~ 0x00007FFFFFFFFFFF0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。图示如下:

clip_image008[4]

对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:

  • Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
  • Data:这里存放的是初始化过的全局变量
  • BSS:这里存放的是未初始化的全局变量
  • Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
  • Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
  • Stack:这是栈区域,自高地址向低地址增长

一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:

clip_image009[4]

Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。

要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brksbrk系统调用操作break指针。两个系统调用的原型如下:

  1. int brk(void *addr);
  2. void *sbrk(intptr_t increment);

brkbreak指针直接设置为某个地址,而sbrkbreak从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errnoENOMEMsbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1

一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。”

        通过上面的讲解之后,我们知道向操作系统请求内存块其实就是移动break指针,但这是一个开销很大的操作,我们不希望每次都调用morecore函数,基于这个考虑morecore函数请求至少NALLOCHeader单元长度,这个较大的块将根据需要分成多个较小的块

#define NALLOC 0//1024    /* minimum #units to request */static Header *morecore(unsigned nu){    char *cp;    Header *up;    if(nu < NALLOC)        nu = NALLOC;    cp = sbrk(nu * sizeof(Header));    if(cp == (char *)-1)    /* no space at all*/        return NULL;    up = (Header *)cp;    up->s.size = nu;    free((void *)(up+1));//将刚申请的数据区插入到链表中    return freep;}

最后分析free函数的实现,由上面sbrk的实现可知内存块的地址是从低到高排列的,我们定义最低的地址为链表的开始,最高的地址为链表的末尾,在free函数中会遍历链表确定要释放的地址处于哪两个相邻的空闲块之间,也可能处于链表的末尾,找到后将其插入到这两个空闲块之间,如果被释放的块与空闲块相邻,那么将其与空闲块合并。

void free(void *ap){    Header *bp,*p;    bp = (Header *)ap -1; /* point to block header */    for(p=freep;!(bp>p && bp< p->s.ptr);p=p->s.ptr)        if(p>=p->s.ptr && (bp>p || bp<p->s.ptr))//释放的空间在链表末尾            break;    /* freed block at start or end of arena*/    if (bp+bp->s.size==p->s.ptr) {    /* join to upper nbr */        bp->s.size += p->s.ptr->s.size;        bp->s.ptr = p->s.ptr->s.ptr;    } else        bp->s.ptr = p->s.ptr;     if (p+p->s.size == bp) {     /* join to lower nbr */        p->s.size += bp->s.size;        p->s.ptr = bp->s.ptr;    } else        p->s.ptr = bp;    freep = p;//返回前一个相邻的空闲块}

这是malloc的一个简单实现,以后可能会回来再看看glibcvs里的实现