全部博文(73)
分类: LINUX
2009-04-23 16:52:49
4 chuck的组织
4.1 chuck
4.2 chunk中的空间复用
5 空闲 chunk 容器
5.1 Bins
5.2 Fastbins
5.3 Unsorted
Bins
5.4 例外的 chunk
6 sbrk
& mmap
6.1 sbrk
6.2 mmap
C语言提供了动态内存管理功能, 在C语言中, 程序员可以使用
malloc() 和 free() 函数显式的分配和释放内存.
关于 malloc() 和free() 函数, C语言标准只是规定了它们需要实现的功能, 而没有对实现方式有什么限制, 这多少让那些追根究底的人感到有些许迷茫, 比如对于 free()函数, 它规定一旦一个内存区域被释放掉, 那么就不应该再对其进行任何引用,任何对释放区域的引用都会导致不可预知的后果 (unperdictable effects).那么,到底是什么样的不可预知后果呢? 这完全取决于内存分配器(memory allocator)使用的算法.这篇文章试图对 Linux glibc提供的allocator的工作方式进行一些描述,并希望可以解答上述类似的问题.虽然这里的描述局限于特定的平台,但一般的事实是,相同功能的软件基本上都会采用相似的技术.这里所描述的原理也许在别的环境下会仍然有效.另外还要强调的一点是,本文只是侧重于一般原理的描述,而不会过分纠缠于细节,如果需要特定的细节知识,请参考特定allocator的源代码.最后,本文描述的硬件平台是Intel
80x86,其中涉及的有些原理和数据可能是平台相关的. 因为只是草草看了ptmalloc的源代码, 并做了一些实验,而没有仔细分析代码. 所以文章中的一些内容难免不实,甚至为虚妄.实在是因为水平有限,并非存心妄自揣测,来愚人耳目.如果读者发现其中有任何错误,请来信告之,并欢迎来信讨论.另外,文章中涉及一些阙值, 比如内存分配的位置,以及
max_fast 大小等等,会因具体的实现而异,若与所述有出入,请自己判断原因.
Linux 程序载入内存后, loader会把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决于 link editor(ld), 在我的机器上是0x8048000, 即
图 1: Linux程序内存分布示意图
GNU Libc 的内存分配器( allocator )
— ptmalloc起源于 Doug Lea 的 malloc (请参看[]).
ptmalloc实现了malloc(),free()以及一组其它的函数.以提供动态内存管理的支持. allocator处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序,为了保持高效的分配,allocator一般都会预先分配一块大于用户请求的内存,并通过某种算法管理这块内存.来满足用户的内存分配要求,用户free掉的内存也并不是立即就返回给操作系统, 相反,allocator会管理这些被free掉的空闲空间,以应对用户以后的内存分配要求.也就是说,allocator不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,allocator 会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存.为实现一个高效的allocator,需要考虑很多的因素.比如, allocator本身管理内存块所占用的内存空间必须很小,分配算法必须要足够的快.Jonathan Bartlett给出了一个简单的allocator 实现[],
事先看看或许会对理解本文有所帮助. 另外插一句,
Jonathan Bartlett 的书“Programming from Ground Up”对想要了解 linux 汇编和工作方式的入门者是个不错的选择.
不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在
ptmalloc 中都使用一个 chunk 来表示. 用户调用 free() 函数释放掉的内存也并不是立即就归还给操作系统, 相反, 它们也会被表示为一个chunk, ptmalloc 使用特定的数据结构来管理这些空闲的 chuck.
ptmalloc 在给用户分配的空间的前后加上了一些控制信息, 用这样的方法来记录分配的信息, 以便完成分配和释放工作. 一个使用中的chuck( 使用中, 就是指还没有被free掉 ) 在内存中的样子如图所示.
图 2: 使用中的chuck
在图中, chunk指针指向一个chunk的开始,一个chunk中包含了用户请求的内存区域和相关的控制信息.图中的mem指针才是真正返回给用户的内存指针. chunk的第二个域的最低一位为p, 它表示前一个块是否在使用中,p为0则表示前一个chunk为空闲,这时chunk的第一个域
prev_size才有效,prev_size表示前一个chunk的size,程序可以使用这个值来找到前一个 chunk 的开始.当p为1时,表示前一个chunk正在使用中,prev_size无效,程序也就不可以得到前一个chunk的大小.而不能对前一个chunk进行任何操作.
ptmalloc 分配的第一个块总是将p设为1, 以防止程序引用到不存在的区域.空闲chunk 在内存中的结构如图所示,
图 3: 空闲的chunk
当chunk空闲时,原本是用户数据区的地方存储了两个指针,指针 fd 指向后一个空闲的chunk, 而 bk指向前一个空闲的chunk, ptmalloc通过这两个指针将大小相近的chunk连成一个双向链表.而不同的 chunk 链表又是通过 bins 或者 fastbins 来组织的(bins 在第节介绍, fastbins 在第节介绍).
为了使得chunk所占用的空间最小, ptmalloc使用了空间复用,一个chunk或者正在被使用,或者已经被 free 掉,所以chunk中的一些域可以在使用状态和空闲状态表示不同的意义, 来达到空间复用的效果. 空闲时,一个chunk中至少要4个size_t大小的空间,用来存储 prev_size, size, fd和bk (见图所
示).也就是16 bytes. chuck的大小要align到8 bytes.当一个chunk
处于使用状态时, 它的下一个 chunk 的 prev_size 域肯定是无效的. 所以实际上, 这个空间也可以被当前 chunk 使用. 这听起来有点不可思议, 但确实是合理空间复用的例子. 故而实际上,一个使用中的chunk的大小的计算公式应该是:
[xleftmargin=
用户free掉的内存并不是都会马上归还给系统,相反,ptmalloc会统一管理 heap中的空闲的 chunk (关于heap, 请参照第节中图),
当用户进行下一次分配请求时, ptmalloc 会首先试图在
heap 中空闲的 chunk 中挑选一块给用户, 这样就避免了频繁的系统调用, 降低了内存分配的开销. ptmalloc将heap中相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个bin. ptmalloc 共维护了128个bin, 并使用一个数组来存储这些 bin(如图).
图 4: bins 结构示意图
数组中的前64个 bin 称为 “exact
bins”, “exact bins” 中的 chunk 具有相同的大小. 两个相邻的bin中的chunk大小相差8 bytes. “exact bins”中的chunk按照最近使用顺序进行排列, 最后释放的 chunk 被链接到链表的头部, 而allocation是从尾部开始, 这样,每一个chunk都有相同的机会被ptmalloc选中.后面的bin被称作“ordered
bins”.“ordered bins”中的每一个bin分别包含了一个给定范围内的chunk,其中的 chunk 按大小序排列.相同大小的 chunk 同样按照最近使用顺序排列. ptmalloc 使用 “smallest-first, best-fit” 原则在空闲 “ordered bins”中查找合适的 chunk.
当空闲的chunk被链接到bin中的时候,
ptmalloc会把表示该chunk是否处于使用中的标志p设为0(注意,这个标志实际上处在下一个chunk中),同时ptmalloc还会检查它前后的chunk是否也是空闲的, 如果是的话, ptmalloc会首先把它们合并为一个大的chunk,然后将合并后的 chunk放到bin中. 要注意的是,并不是所有的 chunk 被释放后就立即被放到bin中. ptmalloc 为了提高分配的速度, 会把一些小的的 chunk 先放到一个叫做 fastbin的容器内.
一般的情况是,程序在运行时会经常需要分配和释放一些较小的内存空间.当allocator合并了相邻的几个小的chunk之后,也许马上就会有另一个小块内存的请求,这样allocator又需要从大的空闲内存中分出一块出来,这样无疑是比较低效的,故而,ptmalloc中在分配过程中引入了fastbins,不大于max_fast(72 bytes)的chunk被free后,首先会被放到fastbins中,fastbins中的chunk并不改变它的使用标志p.这样也就无法将它们合并,当需要给用户分配的chunk小于或等于max_fast时,ptmalloc首先会在fastbins中查找相应的空闲块(具体的分配算法请参考第节),然后才会去查找bins中的空间chunk.在某个特定的时候,ptmalloc会遍历fastbins 中的chunk,将相邻的空闲chunk进行合并,并将合并后的chunk放到bins中去.
如果被用户释放的 chunk 大于 max_fast, 则按上面的叙述它应该会被放到 bins中. 但实际上,
ptmalloc 还引入了一个称为 “unsorted bins”的队列. 这些大于 max_fast 的chunk
首先会被放到 “unsorted bins” 队列中, 在进行 malloc 操作的时候, 如果在
fastbins 中没有找到合适的 chunk, 则
ptmalloc 会先在 “unsorted bins”中查找合适的空闲 chunk, 然后才查找 bins. 如果 “unsorted bins” 不能满足分配要求. malloc 便会将 “unsorted bins” 中的 chunk 放到 bins 中, 然后再在 bins 中继续进行查找和分配过程. 从这个过程可以看出来, “unsorted bins”可以看做是 bins 的一个缓冲区, 增加它只是为了加快分配的速度, 忽略它对我们理解 ptmalloc 没有太大的影响, 在本文中, 这个过程就不被考虑了.
并不是所有的chunk都按照上面的方式来组织,实际上有两种例外情况.
top chunk
在前面一直提到,
ptmalloc 会预先分配一块较大的空闲内存(也就是所为的
heap), 而通过管理这块内存来响应用户的需求, 因为内存是按地址从低向高进行分配的, 在空闲内存的最高处, 必然存在着一块空闲chunk,叫做“top chunk”.当bins和fastbins 都不能满足分配需要的时候, ptmalloc会设法在“top chunk”中分出一块内存给用户, 如果“top chunk”本身不够大,
则ptmalloc会适当的增加它的大小(也就增加了heap的大小).以满足分配的需要,实际上, top chunk在分配时总是在fastbins和bins之后被考虑,所以,不论“top chunk”有多大,它都不会被放到fastbins或者是bins中.
mmaped chunk
当需要分配的chunk足够大,而且fastbins和bins都不能满足要求,甚至“top chunk”本身也不能满足分配需求时, ptmalloc会使用mmap来直接使用内存映射来将页映射到进程空间(具体的情况,请参考第节). 这样分配的chunk在被free时将直接解除映射,于是就将内存归还给了系统,再次对这样的内存区的引用将导致一个“segmentation fault”错误.这样的 chunk 也不会包含在任何bin中.
ptmalloc使用两种方法向内存索取内存空间:sbrk和mmap.它们用于不同的场合.
如图所示,
图 5: 使用 sbrk 和 mmap 分配内存示意图
.bss段之上的这块分配给用户程序的空间被称为heap. start_brk指向heap的开始,而brk指向heap的顶部.可以使用系统调用brk和sbrk来增加标识heap顶部的brk值,从而线性的增加分配给用户的heap空间.在使用malloc之前,brk的值等start_brk,也就是说heap大小为0. ptmalloc在开始时,若请求的空间小于DEFAULT_MMAP_THRESHOLD(128K bytes)时,ptmalloc会调用sbrk增加一块大小为(128KB+chunk_size) align 4K的空间作为heap.这就是前面所说的 ptmalloc所维护的分配空间,当用户请求内存分配时,首先会在这个区域内找一块合适的chunk 给用户.当用户释放了heap中的chunk时, ptmalloc 又会使用fastbins和bins来组织空闲 chunk.以备用户的下一次分配(具体的分配过程见第节).若需要分配的chunk大小小于
DEFAULT_MMAP_THRESHOLD,而heap空间又不够,则此时ptmalloc会通过sbrk调用来增加heap 值,也就是增加“top
chunk”的大小, 每次heap增加的值都会align到4k bytes.
当用户的请求超过DEFAULT_MMAP_THRESHOLD ,并且使用sbrk分配失败的时候,ptmalloc会尝试使用mmap直接映射一块内存到进程内存空间(我机器上是在0x40159000地址处).使用mmap直接映射的chunk在释放时直接解除映射,而不再属于进程的内存空间.任何对该内存的访问都会产生段错误.而在heap中分配的空间则可能会留在进程内存空间内, 还可以再次引用(当然是很危险的).
ptmalloc 的响应用户内存分配要求的具体步骤为:
3.
判断所需分配chunk的大小是否满足chunk_size <=max_fast(max_fast默认为72
bytes), 如果是的话,则转下一步,否则跳到第5步.
4.
首先尝试在 fastbins 中摘取一个所需大小的 chunk 分配给用户. 如果可以找到,
则分配结束. 否则转到下一步.
5.
判断所需大小是否处在“exact
bins” 中, 即判断 chunk_size 512
bytes 是否成立(见图).
如果 chunk 大小处在 “exact bins”中, 则转下一步, 否则转到第6步.
总结一下: 根据用户请求分配的内存的大小, ptmalloc 有可能会在两个地方为用户分配内存空间.在第一次分配内存时,brk值等于start_brk,所以heap和top chunk大小都是0.这时,如果不增加heap大小,就不能满足任何分配要求.所以若用户的请求小于 DEFAULT_MMAP_THRESHOLD,则ptmalloc会初始化heap.然后在heap中分配空间给用户,以后的分配就基于这个heap进行. 若第一次用户的请求就大于DEFAULT_MMAP_THRESHOLD,则ptmalloc直接使用mmap分配一块给用户,而heap也就没有被初始化,直到用户第一次请求小于DEFAULT_MMAP_THRESHOLD的内存分配. 第一次以后的分配就比较复杂了,简单说来,ptmalloc首先会查找fastbins,如果不能找到匹配的chunk,则查找exact bins.若还是不行,则查找sorted bins.在
fastbins 和exact bins中的查找都需要精确匹配,
而在sorted bins中查找时,则遵循smallest-first, best-fit的原则, 不需要精确匹配. 若以上方法都失败了, 则ptmalloc会考虑使用top chunk.若top chunk 也不能满足分配要求.而且所需chunk大小大于DEFAULT_MMAP_THRESHOLD
,则使用mmap进行分配.否则增加heap.增大top chunk以满足分配要求.
free()函数接受一个指向分配区域的指针作为参数, 释放该指针所指向的 chunk. 而具体的释放方法则看该 chunk 所处的位置和该 chunk 的大小. free()函数的工作步骤如下:
[1] |
Doug Lea. A Memory Allocator. .
|
[2] |
Jonathan Bartlett. 内存管理内幕—动态分配的选择、折衷和实现. http://www-128.ibm.com/developerworks/cn/linux/l-memory/
|
E-mail:phenixsen@gmail.com