一直对malloc的实现不是很懂,K&R的C程序设计语言的8.7节讲了一个malloc的基本实现,这篇文章主要把自己的理解记录一下。
malloc是在堆里申请空间的,每一次申请的空间叫做内存块,每个内存块包括头部和数据区,空闲的内存块通过一个循环链表组织在一起,内存块的地址是从低到高排列的,这是由堆的地址是从低到高生长决定的,链表有一个表头,表头不包含数据区。
为了简化块的对齐,每个内存块的大小都是头部的整数倍,头部通过一个联合体来实现,包括指向下一个块的指针和当前块的长度:
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 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF,其中前面为用户空间(User Space),后者为内核空间(Kernel Space)。图示如下:
对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:
- Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
- Data:这里存放的是初始化过的全局变量
- BSS:这里存放的是未初始化的全局变量
- Heap:堆,这是我们本文重点关注的地方,堆自低地址向高地址增长,后面要讲到的brk相关的系统调用就是从这里分配内存
- Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域自高地址向低地址增长
- Stack:这是栈区域,自高地址向低地址增长
一般来说,malloc所申请的内存主要从Heap区域分配(本文不考虑通过mmap申请大块内存的情况)。进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存。Linux对堆的管理示意如下:
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
要增加一个进程实际的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:
- int brk(void *addr);
- void *sbrk(intptr_t increment);
brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量。brk在执行成功时返回0,否则返回-1并设置errno为ENOMEM;sbrk成功时返回break移动之前所指向的地址,否则返回(void *)-1。
一个小技巧是,如果将increment设置为0,则可以获得当前break的地址。”
通过上面的讲解之后,我们知道向操作系统请求内存块其实就是移动break指针,但这是一个开销很大的操作,我们不希望每次都调用morecore函数,基于这个考虑morecore函数请求至少NALLOC个Header单元长度,这个较大的块将根据需要分成多个较小的块
#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的一个简单实现,以后可能会回来再看看glibc和vs里的实现