1、malloc实现
在malloc分配空间时是在Heap上分配的,实质上,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的地址。 typedef union header Header;
static Header base;/*从空链表开始*/
static Header *freep = NULL;/*空闲链表的初始指针*/
#define NALLOC 1024 /* 最小申请单元数 */
static Header *morecore(unsigned nu)
{
char *cp;
Header *up;
if(nu < NALLOC)
nu = NALLOC;
cp = sbrk(nu * sizeof(Header));
if(cp == (char *)-1) /* 没有空间*/
return NULL;
up = (Header *)cp;
up->s.size = nu;
free((void *)(up+1));
return freep;
}
void *malloc(unsigned nbytes)
{
Header *p, *prevp; Header *morecore(unsigned);
unsigned nunits;
nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1;
if((prevp = freep) == NULL) /* 没有空闲链表 */
{
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}
for(p = prevp->s.ptr; ;prevp = p, p= p->s.ptr)
{
if(p->s.size >= nunits) /* 足够大 */
{
if (p->s.size == nunits) /* 正好 */
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) /* 闭环的空闲链表*/
if ((p = morecore(nunits)) == NULL)
return NULL; /* 没有剩余的存储空间 */
}
}
(1)malloc实际分配的空间是Header大小的整数倍,并且多出一个Header空间用于放置Header
(2)式nunits = (nbytes+sizeof(Header)-1)/sizeof(Header) + 1中的减1是为了防止(nbytes+sizeof(Header))%sizeof(Header) == 0时,多分配了一个Header大小的空间
(3)第一次调用malloc函数时,freep为NULL,系统将创建一个退化的空闲链表,它只包含一个大小为0的块,且该块指向自己。任何时候,当请求空闲空间时,都将搜索空闲块链表。搜索从上一次找到空闲块的地方(freep)开始。该策略可以保证链表是均匀的。如果找到的块太大,则将其尾部返回给用户,这样,初始块的头部只需要修改size字段即可。
(4)任何时候,返回给用户的指针都指向块内的空闲存储空间,即比指向头部的指针大一个单元。
(5)sbrk不是系统调用,是C库函数。sbrk/brk是从堆中分配空间,本质是移动一个位置,向后移就是分配空间,向前移就是释放空间,sbrk用相对的整数值确定位置,如果这个整数是正数,会从当前位置向后移若干字节,如果为负数就向前若干字节。在任何情况下,返回值永远是移动之前的位置。在LINUX中sbrk(0)能返回比较精确的虚拟内存使用情况。