最近看了几个malloc和free的问题,总结一下。
一般来讲,它们是C Standard Library提供的而不是由操作系统的内核实现。例如微软的是msvcrt,Linux下是glibc。当然也有第三方库函数,比如jemalloc、tcmalloc。所以每个malloc和free的实现都有所不同,而且这些实现和内核之间是保留有一定自由度的。比如每次malloc是多大空间,每次free之后又归还多少给OS,都是依据不同实现而定的。本文malloc/free的讨论都是基于Linux/glibc的。
在操作系统的课程中讲到过空闲内存管理的策略,一种方式是使用位图,用一个bit代表固定大小的一片内存区域(比如4B),0表示未分配,1表示已分配。这种方法存在一些问题:
第二种方式是使用链表。将内存中的每个已分配区域和未分配区域做成节点形式,最终形成一个链表。这种方法的优点如下:
linux采用的是glibc中堆内存管理ptmalloc实现,虚拟内存的布局规定了malloc申请位置以及大小,malloc一次性能申请小内存(小于128KB),分配的是在堆区(heap),用sbrk()进行对齐生长,而malloc一次性申请大内存(大于128KB时)分配到的是在映射区,而不是在堆区,采用的mmap()系统调用进行映射。malloc的实现与物理内存是无关的,内核为每个进程维护一张页表,页表存储进程空间内每页的虚拟地址,页表项中有的虚拟内存页对应着某个物理内存页面,也有的虚拟内存页没有实际的物理页面对应。无论malloc通过sbrk还是mmap实现,分配到的内存只是虚拟内存,而且只是虚拟内存的页号,代表这块空间进程可以用,实际上还没有分配到实际的物理页面。等你的进程访问到这个新分配的内存空间的时候,如果其还没有对应的物理页面分配,就会产生缺页中断,内核这个时候会给进程分配实际的物理页面,以与这个未被映射的虚拟页面对应起来。
链表的方法只代表了一种思路,真正使用这种方法是有很多缺点的,最大的问题就内存碎片。如果malloc申请的区域都不大,那么大片的空闲区域会不断裂开,形成gap。
glibc中的malloc实现是基于链表的思想,但远远不止上面那么简单。glibc中是用ptmalloc来管理内存的。不管内存在哪里分配,用什么方法分配,用户请求分配的空间在ptmalloc中都是用一个chunk来表示。用户调用free()之后释放的内存也不会立即返回给操作系统,相反,它们会被表示成一个chunk,ptmalloc使用特定的数据结构来管理这些chunk。一个正在使用的chunk结构大概如下:
其中,mem才是返回给用户的指针。一个空闲的chunk结果大概如下:
这样chunk和chunk之间就形成了双向链表。
上面说了,如果仅仅形成链表是不够的。因为操作系统分配和回收内存的开销是非常大的,所以用户free掉的内存并不是马上归还给操作系统,试想如果每次free1B都要归还,那就太尴尬了。所以free的内存被进行统一管理。当用户进行下一次分配的时候,会试图首先在空闲chunk中挑选一块给用户,这样就避免了频繁的系统调用,降低内存开销。ptmalloc将相似大小的chunk用双向链表链接起来,形成一个bin,这些bin用一个一维数组表示:
数组前64个bin存放小块的chunk的链表,大的chunk链表都放在后面。当一个chunk被free掉的时候,ptmalloc会检查他的前后chunk是否也是空闲,如果是的话,就把他们合并成一个大的chunk,放在unsorted bin中。
一般情况下,程序申请的chunk都比较小,如果free掉之后合并这些chunk,然后再申请了小块内存,那需要再次裂开节点,这样无疑是低效的。所以小块内存一般都会被存在fast bin中,这里的chunk是不合并的,用于满足小块内存申请的需求。不大于max_fast(通常为64B)的chunk被释放后,通常会首先进入fast bin。程序会先查找fast_bin,然后再查找bins。
unsorted bins是bins的第一个区域。如果在fast bins中没有找到合适的chunk,则会到unsorted bins中进行寻找。如果在unsorted bins中也未找到,将unsorted bins中的chunk放回到bins中,再到bins中寻找。所以其实unsorted bins可以看作bins的一个缓冲区。
并非所有chunk都按上述方式组织,事实上除此之外,还有top chunk,mmaped chunk和last remainder。本文也只是简单介绍一下管理chunk的数据结构,基本上可以解释两个问题:
1.一般来讲,小内存的分配速度要比大内存速度快些。
2.并非malloc多少就给多少,实际在内存中要多占一些空间,因为有chunk。
3.一般来讲,free掉的内存并非马上还给操作系统,而是留着下一次进行分配。
至于具体的分配和释放操作流程是比较复杂的,本处就暂且先按下不表了