malloc,free,calloc,reallocde的区别连系
malloc的相关概念:
## void *malloc(size_t n)
1.参数size_t :无符号整形 malloc()是C语言中动态存储管理 的一组标准库函数之一。其作用是在内存的动态存储区中分配一个长度为size的连续空间。其参数是一个无符号整形数。
2.返回值是一个指向所分配的连续存储域的起始地址的指针。还有一点必须注意的是,当函数未能成功分配存储空间(如内存不足 )就会返回一个NULL指针。所以在调用该函数时应该检测返回值是否为NULL并执行相应的操作。
3.虽然这里的存储块是通过动态分配得到的,但 是它的大小也是确定的,同样不允许越界使用,越界使用动态分配的存储 块,尤其是越界赋值,可能引起非常严重的后果,通常会破坏程序的运行系统,可能造成本程序或者整个计算机系统 垮台。
malloc 内部机理:
首先看一下我们先看一下内存分布(这里为虚拟内存的分布):
对用户来说,主要关注的空间是User Space。将User Space放大后,可以看到里面主要分为如下几段:
Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码)
Data:这里存放的是初始化过的全局变量
BSS:这里存放的是未初始化的全局变量
Heap:堆,这是我们本文重点关注的地方,堆自 高地址向低地址 增长,后面要讲到的brk相关的系统调用就是从这里分配内存
Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存区域,本文不讨论这种情况。这个区域 自高地址向低地址增长
Stack:这是栈区域,自高地址向低地址增长
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
A. 一般来说,malloc所申请的内存主要从Heap区域分配,底层是系统调用brk函数。
brk是将数据段(.data)的最高地址指针_edata往高地址推,,而sbrk将指针从当前位置移动increment所指定的增量,
一个小技巧是,如果将increment设置为0,则可以获得当前break的地址
int brk(void *addr);
void *sbrk(intptr_t increment);
B . malloc所申请的内存主要从 Mapping Area 区域分配
mmap是在进程的虚拟地址空间中(一般是堆和栈中间Mapping Area)找一块空闲的地址 使用的条件在malloc 需要内存大于128k时候,就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。
因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的)mmap的内存可以单独释放
理由说明:
进程调用free(C)以后,C对应的虚拟内存和物理内存一起释放 malloc调用mmap分配内存,请求结束的时候,调用munmap释放内存。
进程调用free(B)以后,B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,如果往回推,那么D这块内存怎么办呢?当然,B这块内存,是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。 进程调用free(D)以后,B和D连接起来,变成一块140K的空闲内存。
进程所面对的虚拟内存地址空间,只有按页映射到物理内存地址,才能真正使用。受物理存储容量限制,整个堆虚拟内存空间不可能全部映射到实际的物理内存
Linux维护一个break指针,这个指针指向堆空间的某个地址。从堆起始地址到break之间的地址空间为映射好的,可以供进程访问;而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。
calloc的实现:
void *calloc(size_t nmemb, size_t size);
1.malloc的实现,申请内存
2.数据区内容置0
function allocates memory for an array of nmemb elements
of size bytes each and returns a pointer to the allocated memory. The
memory is set to zero. 函数返回一个指针指向分配的内存,内存数据值为0,内存的大小为nmemb个大小为size的数组成员.
realloc的实现:
void *realloc(void *ptr, size_t size);
The realloc() function changes the size of the memory block pointed to
by ptr to size bytes. The contents 内容 will be unchanged in the range 范围 from
the start of the region 范围 up to the minimum 最小值 of the old and new sizes. If
the new size is larger than the old size, the added memory will not be
initialized 初始化
realloc函数 先判断当前的指针是否有足够的连续空间,如果有,扩大ptr指向的地址,并且将ptr返回,如果空间不够,先按照size指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来ptr所指内存区域(注意:原来指针是自动释放,不需要使用free),同时返回新分配的内存区域的首地址。即重新分配存储器块的地址
最后mem_address所指内存区域的大小为newsize长度。
free函数机理:
free的实现并不像看上去那么简单,这里我们要解决两个关键问题:
1.如何验证所传入的地址是有效地址,即确实是通过malloc方式分配的数据区首地址
2.如何解决碎片首先我们要保证传入free的地址是有效的,这个有效包括两方面:
第一个问题:只需要比较地址就行
在结构体内增加一个magic pointer,这个指针指向数据区的第一个字节(也就是在合法时free时传入的地址),我们在free前检查magic pointer是否指向参数所指地址。
第二个问题:
当多次malloc和free后,整个内存池可能会产生很多碎片block,这些block很小,经常无法使用,甚至出现许多碎片连在一起,虽然总体能满足某此malloc要求,但是由于分割成了多个小block而无法fit,这就是碎片问题。当free某个block时,如果发现它相邻的block也是free的,则将block和相邻block合并。
以下代码说明:
free的实现思路就比较清晰了:首先检查参数地址的合法性,如果不合法则不做任何事;否则,将此block的free标为1,并且在可以的情况下与后面的block进行合并。如果当前是最后一个block,则回退break指针释放进程内存,如果当前block是最后一个block,则回退break指针并设置first_block为NULL。合并过程中用到双向链表。实现如下:
void free(void *p) {
t_block b;
//判断地址合法
if(valid_addr(p)) {
b = get_block(p);
b->free = 1;
if(b->prev && b->prev->free)
//进行合并
b = fusion(b->prev);
if(b->next)
fusion(b);
else {
if(b->prev)
b->prev->prev = NULL;
else
first_block = NULL;
brk(b);
}
}
}
int valid_addr(void *p) {
if(first_block) {
if(p > first_block && p < sbrk(0)) {
return p == (get_block(p))->ptr;
}
}
return 0;
}
t_block fusion(t_block b) {
if (b->next && b->next->free) {
b->size += BLOCK_SIZE + b->next->size;
b->next = b->next->next;
if(b->next)
b->next->prev = b;
}
return b;
}