malloc()和free()是c中两个非常基本的函数,但这种最基本的东西往往都是特别复杂的。
malloc和free的原形如下:
void *malloc(unsigned int num_bytes);
void free(void *ptr);
在c的标准中并没有定义这两个函数的具体实现,在我们最常用的gcc中,malloc使用的是ptmalloc的实现,最早由Doug Lea完成,并被Wolfram Gloger改进以支持多线程。
malloc的具体实现过于复杂,这里就不具体展开了。
总体上说,ptmalloc的内存管理是基于内存池的,而它的内存来源有两种:
1 通过brk()获得
2 通过mmap()匿名映射获得
这两种获得内存的方式分别对应于进程地址空间的不同部分。64位linux进程的默认内存布局如下:
对于其中的Heap区域,就是我们通常意义上的堆空间,这部分内存将通过brk()获得。brk()的原理非常简单,只涉及指针的上下移动,也就是只分配虚拟地址空间,而不分配物理内存,当实际访问到此内存时,将触发page fault异常由操作系统完成物理内存的分配。由于操作简单,所以brk的效率非常高。
而另一个Memory Mapping Region区域,简称mmap区域,将通过操作系统的mmap()函数获得。mmap区域不仅可以提供内存的分配,还可以映射文件,比如通过mmap打开文件,或者加载so文件等等。
mmap函数的原形如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
其中的flags,在使用mmap分配内存时,设置为MAP_ANONYMOUS,也就是匿名映射。当使用mmap()映射内存时,操作系统默认将对这块内存执行清0操作,因此相比于brk,mmap的效率相对较低。
基于内存池的内存管理,本质上都是把变长的内存分配转换为定长的内存分配。因为只有定长,才可以复用。
ptmalloc的内存池结构大致如下:
可以看出,内存的分配将根据实际的大小,选择定长块。比如请求的内存是23字节,那么将分配一块24字节的内存,而请求的如果是25字节,那么将分配一块32字节的内存(对于大于512字节的数据,统一存储在large bin中)。具体的分配策略就不展开了。
每一种大小的内存,都组成了一个一个的队列,在这些队列里维护了一个双向链表,将所有的小块内存串联起来。每一个小块内存(chunk)的结构如下:
上面的这个结构是一块在使用中的内存的状态。其中的mem部分,则是返回给用户的void*指针位置。而最开始的两个结构:size of previous chunk,和 size of chunk,则用于维护全局内存的链表。
之所以说是全局内存的链表,是由于内存分配时是先向系统请求一个比较大的内存块(64位系统一般为64Mb),之后从这64Mb内存中切出用户需要的大小分配给用户。而为了维护分配出去的内存块之间的关系,通过前两个结构来使所有内存块构成一个大的链表,当回收内存时,通过这个全局链表,将所有空闲内存组合起来,还给操作系统,其中有3个标记位:A,M,P,P标识前一块内存是否空闲。
下面的结构就是一块空闲内存的状态:
相比于使用中的状态,空闲部分的内存增加了4个新的结构(Forward pointer to next chunk in list 等,其中fd_nextsize和bk
_nextsize只存在于large bin中),这4个结构用于维护每个定长内存队列的双向链表结构,这个链表的存在主要是为了分配时查找内存时足够便利,可以基本上保证分配内存时的平均复杂度维持在O(1)。
在有了前面所有的介绍之后,可以总体上描述一下malloc和free的基本流程:
当用户向ptmalloc请求内存时:
1 首先查找定长内存分配池,如果查找到则返回
2 如果没有空闲内存可供使用,则向操作系统申请一块64Mb的内存,从中切出用户需要的内存,返回
当用户调用free释放内存时:
1 直接将内存放入适当的定长内存池队列
2 如果触发了一定的条件,则将所有空闲内存合并,如果满足释放条件,将内存全部还给操作系统
当然了,上面的描述中省略了太多的细节。比如什么时候走brk什么时候走mmap, 再比如当请求的内存大于一个阙值时,ptmalloc将会变成一个mmap的简单封装,还有触发内存归还操作系统的条件等等。
不过已经足够回答题目中的问题了:因为malloc的时候记录了大小。
这里还可以得出另一个结论:由于malloc的时候记录了大量的状态,所以在频繁使用malloc分配小内存时,会造成大量的内存浪费。举例来说,当反复malloc(1)时,每一次分配的内存在32字节:包括size of previous chunk,size of chunk,bk_chunk_pointer,fd_chunk_pointer共4个指针,合计4 * 8 = 32字节....