ptmalloc

ptmalloc

phenix *

2006-06-07html

目录

    1  前言
node

    2  x86平台Linux程序的内存分布
linux

    3  Allocator
程序员

    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
安全

    7  malloc()
数据结构

    8  free()
函数

1  前言

C语言提供了动态内存管理功能, 在C语言中, 程序员可使用 malloc() 和 free() 函数显式的分配和释放内存. 关于 malloc() 和free() 函数, C语言标准只是规定了它们须要实现的功能, 而没有对实现方式有什么限制, 这多少让那些追根究底的人感到有些许迷茫, 好比对于 free() 函数, 它规定一旦一个内存区域被释放掉, 那么就不该该再对其进行任何引用, 任何对释放区域的引用都会致使不可预知的后果 (unperdictable effects). 那么, 究竟是什么样的不可预知后果呢? 这彻底取决于内存分配器(memory allocator)使用的算法. 这篇文章试图对 Linux glibc 提供的 allocator 的工做方式进行一些描述, 并但愿能够解答上述相似的问题. 虽然这里的描述局限于特定的平台, 但通常的事实是, 相同功能的软件基本上都会采用类似的技术. 这里所描述的原理也许在别的环境下会仍然有效. 另外还要强调的一点是, 本文只是侧重于通常原理的描述, 而不会过度纠缠于细节, 若是须要特定的细节知识, 请参考特定 allocator 的源代码. 最后, 本文描述的硬件平台是 Intel 80x86, 其中涉及的有些原理和数据多是平台相关的. 操作系统

由于只是草草看了 ptmalloc 的源代码, 并作了一些实验, 而没有仔细分析代码. 因此文章中的一些内容不免不实, 甚至为虚妄. 实在是由于水平有限, 并不是存心妄自揣测, 来愚人耳目. 若是读者发现其中有任何错误, 请来信告之, 并欢迎来信讨论. 另外, 文章中涉及一些阙值, 好比内存分配的位置, 以及 max_fast 大小等等, 会因具体的实现而异, 若与所述有出入, 请本身判断缘由.

2  x86平台Linux程序的内存分布

Linux 程序载入内存后, loader 会把可执行文件中的各个段依次载入到从某一地址开始的空间中(载入地址取决于 link editor(ld), 在个人机器上是0x8048000, 即128M处). 如图1所 示, 首先被载入的是 “.text” 段, 而后是 “.data” 段, 最后是 “.bss” 段. 这能够看做是程序的开始空间. 程序所能访问的最后的地址是0xbfffffff, 也就是到3G地址处, 3G以上的1G空间是内核使用的, 应用程序不能够直接访问. 应用程序的堆栈从最高地址处开始向下生长, “.bss”段与堆栈之间的空间是空闲的. 这个区域能够供用户自由使用, 可是它在刚开始的时候并无映射到内存空间内, 是不可访问的. 在向内核请求分配该空间以前, 对这个空间的访问会致使一个 “segmentation fault”. 用户程序能够直接使用系统调用来管理这块空间, 但更多的时候都是程序都是使用C语言提供的 malloc() 和 free() 函数来动态的申请和释放内存.

图 1:  Linux程序内存分布示意图

3  Allocator

GNU Libc 的内存分配器( allocator ) — ptmalloc 起源于 Doug Lea 的 malloc (请参看[1]). ptmalloc 实现了 malloc() , free() 以及一组其它的函数. 以提供动态内存管理的支持. allocator 处在用户程序和内核之间, 它响应用户的分配请求, 向操做系统申请内存, 而后将其返回给用户程序, 为了保持高效的分配, allocator 通常都会预先分配一块大于用户请求的内存, 并经过某种算法管理这块内存. 来知足用户的内存分配要求, 用户 free 掉的内存也并非当即就返回给操做系统, 相反, allocator 会管理这些被 free 掉的空闲空间, 以应对用户之后的内存分配要求. 也就是说, allocator 不但要管理已分配的内存块, 还须要管理空闲的内存块, 当响应用户分配要求时, allocator 会首先在空闲空间中寻找一块合适的内存给用户, 在空闲空间中找不到的状况下才分配一块新的内存. 为实现一个高效的 allocator, 须要考虑不少的因素. 好比, allocator 自己管理内存块所占用的内存空间必须很小, 分配算法必需要足够的快. Jonathan Bartlett 给出了一个简单的 allocator 实现[2], 事先看看或许会对理解本文有所帮助. 另外插一句, Jonathan Bartlett 的书 “Programming from Ground Up” 对想要了解 linux 汇编和工做方式的入门者是个不错的选择.

4  chuck的组织

无论内存是在哪里被分配的, 用什么方法分配, 用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示. 用户调用 free() 函数释放掉的内存也并非当即就归还给操做系统, 相反, 它们也会被表示为一个 chunk, ptmalloc 使用特定的数据结构来管理这些空闲的 chuck.

4.1  chuck

ptmalloc 在给用户分配的空间的先后加上了一些控制信息, 用这样的方法来记录分配的信息, 以便完成分配和释放工做. 一个使用中的chuck( 使用中, 就是指尚未被free掉 ) 在内存中的样子如图2所示.

图 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所示,

图 3:  空闲的thunk

当 chunk 空闲时, 本来是用户数据区的地方存储了两个指针, 指针 fd 指向后一个空闲的 chunk, 而 bk 指向前一个空闲的 chunk, ptmalloc 经过这两个指针将大小相近的 chunk 连成一个双向链表. 而不一样的 chunk 链表又是经过 bins 或者 fastbins 来组织的(bins 在第5.1节介绍, fastbins 在第5.2节介绍).

4.2  chunk中的空间复用

为了使得 chunk 所占用的空间最小, ptmalloc 使用了空间复用, 一个 chunk 或者正在被使用, 或者已经被 free 掉, 因此 chunk 的中的一些域能够在使用状态和空闲状态表示不一样的意义, 来达到空间复用的效果. 空闲时, 一个 chunk 中至少要4个 size_t 大小的空间, 用来存储 prev_size, size , fd 和 bk (见图3所 示). 也就是16 bytes. chuck 的大小要 align 到8 bytes. 当一个 chunk 处于使用状态时, 它的下一个 chunk 的 prev_size 域确定是无效的. 因此实际上, 这个空间也能够被当前 chunk 使用. 这听起来有点难以想象, 但确实是合理空间复用的例子. 故而实际上, 一个使用中的 chunk 的大小的计算公式应该是:

[xleftmargin=1cm] in_use_size = ( 用户请求大小 + 8 - 4 ) align to 8 bytes 这里加8是由于须要存储 prev_size 和 size, 但又由于向下一个 chunk “借”了4个bytes, 因此要减去4. 最后, 由于空闲的 chunk 和使用中的 chunk 使用的是同一块空间. 因此确定要取其中最大者做为实际的分配空间. 即最终的分配空间 chunk_size = max(in_use_size, 16). 这就是当用户请求内存分配时, ptmalloc 实际须要分配的内存大小, 在后面的介绍中. 若是不是特别指明的地方, 指的都是这个通过转换的实际须要分配的内存大小, 而不是用户请求的内存分配大小.

5  空闲 chunk 容器

5.1  Bins

用户 free 掉的内存并非都会立刻归还给系统, 相反, ptmalloc 会统一管理 heap 中的空闲的 chunk (关于heap, 请参照第6节中图5), 当用户进行下一次分配请求时, ptmalloc 会首先试图在 heap 中空闲的 chunk 中挑选一块给用户, 这样就避免了频繁的系统调用, 下降了内存分配的开销. ptmalloc 将 heap 中类似大小的 chunk 用双向链表连接起来, 这样的一个链表被称为一个bin. ptmalloc 共维护了128个bin, 并使用一个数组来存储这些 bin(如图4).

图 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的容器内.

5.2  Fastbins

通常的状况是, 程序在运行时会常常须要分配和释放一些较小的内存空间. 当 allocator 合并了相邻的几个小的 chunk 以后, 也许立刻就会有另外一个小块内存的请求, 这样 allocator 又须要从大的空闲内存中分出一块出来, 这样无疑是比较低效的, 故而, ptmalloc 中在分配过程当中引入了 fastbins, 不大于 max_fast (72 bytes) 的 chunk 被 free 后, 首先会被放到 fastbins 中, fastbins 中的 chunk 并不改变它的使用标志p. 这样也就没法将它们合并, 当须要给用户分配的 chunk 小于或等于 max_fast 时, ptmalloc 首先会在 fastbins 中查找相应的空闲块(具体的分配算法请参考第7节), 而后才会去查找 bins 中的空间 chunk. 在某个特定的时候, ptmalloc 会遍历 fastbins 中的 chunk, 将相邻的空闲 chunk 进行合并, 并将合并后的 chunk 放到 bins 中去.

5.3  Unsorted 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 没有太大的影响, 在本文中, 这个过程就不被考虑了.

5.4  例外的 chunk

并非全部的 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 来直接使用内存映射来将页映射到进程空间(具体的状况, 请参考第 6节). 这样分配的 chunk 在被 free 时将直接解除映射, 因而就将内存归还给了系统, 再次对这样的内存区的引用将致使一个 “segmentation fault” 错误. 这样的 chunk 也不会包含在任何 bin 中.

6  sbrk & mmap

ptmalloc 使用两种方法向内存索取内存空间: sbrk 和 mmap. 它们用于不一样的场合.

6.1  sbrk

如图5所示,

图 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增长一块大小为 ( 128 KB + chunk_size ) align 4K 的空间做为heap. 这就是前面所说的 ptmalloc 所维护的分配空间, 当用户请求内存分配时, 首先会在这个区域内找一块合适的 chunk 给用户. 当用户释放了 heap 中的 chunk 时, ptmalloc 又会使用 fastbins 和 bins 来组织空闲 chunk. 以备用户的下一次分配(具体的分配过程见第7节). 若须要分配的 chunk 大小小于 DEFAULT_MMAP_THRESHOLD, 而 heap 空间又不够, 则此时 ptmalloc 会经过 sbrk 调用来增长 heap 值, 也就是增长 “top chunk”的大小, 每次 heap 增长的值都会 align 到4k bytes.

6.2  mmap

当用户的请求超过 DEFAULT_MMAP_THRESHOLD , 而且使用 sbrk 分配失败的时候, ptmalloc 会尝试使用 mmap 直接映射一块内存到进程内存空间(我机器上是在0x40159000地址处). 使用 mmap 直接映射的 chunk 在释放时直接解除映射, 而再也不属于进程的内存空间. 任何对该内存的访问都会产生段错误. 而在 heap 中分配的空间则可能会留在进程内存空间内, 还能够再次引用(固然是很危险的).

7  malloc()

ptmalloc 的响应用户内存分配要求的具体步骤为:

  1. 获取分配区的锁, ptmalloc 对 Doug Lea malloc 的主要扩展即是增长了线程支持. 为了防止多个线程同时访问同一个分配区, 在进行分配以前须要取得分配区域的锁, 若是主分配区域的锁不能获得, 那么会 ptmalloc 会创建一个新的分配区域供当前线程使用.

  2. 将用户的请求大小转换为实际须要分配的空间大小(见第4.2节的相关介绍).

  3. 判断所需分配 chunk 的大小是否知足 chunk_size <= max_fast (max_fast 默认为 72 bytes) , 若是是的话, 则转下一步, 不然跳到第5步.

  4. 首先尝试在 fastbins 中摘取一个所需大小的 chunk 分配给用户. 若是能够找到, 则分配结束. 不然转到下一步.

  5. 判断所需大小是否处在 “exact bins” 中, 即判断 chunk_size 512 bytes 是否成立(见图4). 若是 chunk 大小处在 “exact bins”中, 则转下一步, 不然转到第6步.

  6. 根据所需分配的 chunk 的大小, 找到具体所在的 “exact bins”, 并从该 bin 的尾部摘取一块刚好知足大小的 chunk. 若成功, 则分配结束, 不然, 转到下一步.

  7. 到了这一步, 说明须要分配的是一块大的内存, 或者, “exact bins” 中找不到合适的 chunk. 因而, ptmalloc 首先会遍历 fastbins 中的 chunk , 将相邻的 chunk 进行合并, 并连接到 bins 中, 而后从 “sorted bins” 中按照 “smallest-first, best-fit” 原则, 找一块合适的 chunk, 从中划分一块所需大小的chunk, 并将剩下的部分连接回到 bins 中. 若操做成功, 则分配结束, 不然转到下一步.

  8. 若是搜索 fastbins 和 bins 都没有找到合适的 chunk, 那么就须要操做 top chunk 来进行分配了. 判断 top chunk 大小是否知足所需 chunk 的大小, 若是是, 则从 top chunk 中分出一块来. 不然转到下一步.

  9. 到了这一步, 说明 top chunk 也不能知足分配要求, 因此, 因而就有了两个选择: 调用 sbrk, 增长 top chunk 大小; 或者使用 mmap 来直接分配. 在这里, 须要依靠 chunk 的大小来决定到底使用哪一种方法. 判断所需分配的 chunk 大小是否大于等于 DEFAULT_MMAP_THRESHOLD (128KB), 若是是的话, 则转下一步, 调用 mmap 分配, 不然跳到第11步, 使用 sbrk 来增长 top chunk 的大小.

  10. 使用 mmap 系统调用在大约 0x40159000 (大约为1G) 地址处为程序的内存空间映射一块 chunk_size align 4kB 大小的空间. 而后将内存指针返回给用户.

  11. 判断是否为第一次调用 malloc, 如果, 则须要进行一次初始化工做, 分配一块大小为 (chunk_size + 128K) align 4KB 大小的空间做为初始的 heap. 若已经初始化过了, 则调用 sbrk 增长 heap 空间, 使之知足分配需求, 并将内存指针返回给用户.

总结一下: 根据用户请求分配的内存的大小, ptmalloc 有可能会在两个地方为用户分配内存空间. 在第一次分配内存时, brk 值等于 start_brk, 因此实际上 heap 大小为0, 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. 以知足分配要求.

8  free()

free() 函数接受一个指向分配区域的指针做为参数, 释放该指针所指向的 chunk. 而具体的释放方法则看该 chunk 所处的位置和该 chunk 的大小. free()函数的工做步骤以下:

  1. free() 函数一样首先须要获取分配区的锁, 来保证线程安全.

  2. 判断传入的指针是否为0, 若是为0, 则什么都不作, 直接return. 不然转下一步:

  3. 判断所需释放的 chunk 是否为 mmaped chunk, 若是是, 则直接释放 mmaped chunk, 解除内存空间映射. 该空间再也不有效. 释放完成. 不然跳到下一步.

  4. 判断 chunk 的大小和所处的位置, 若 chunk_size <= max_fast , 而且 chunk 并不位于 heap 的顶部, 也就是说并不与 top chunk 相邻, 则转到下一步, 不然跳到第6步. (由于与 top chunk 相邻的小 chunk 也和 top chunk 进行合并, 因此这里不只须要判断大小, 还须要判断相邻状况.)

  5. 将 chunk 放到 fastbins 中, chunk 放入到 fastbins 中时, 并不设置该 chunk 使用位. 也不与相邻的 chunk 进行合并. 只是放进去, 如此而已. 作实验的结果还发现ptmalloc 放入 fastbins 中的 chunk 中的用户数据去全置为 0. 可是在源代码中找不到相关的代码. 这一步作完以后释放便结束了, 程序从 free() 函数中返回..

  6. 判断前一个 chunk 是否处在使用中, 若是前一个块也是空闲块, 则合并. 并转下一步.

  7. 判断当前释放 chunk 的下一个块是否为 top chunk, 若是是, 则转第9步, 不然转下一步.

  8. 判断下一个 chunk 是否处在使用中, 若是下一个 chunk 也是空闲的. 则合并, 并将合并后的 chunk 放到 bins 中. 注意, 这里在合并的过程当中, 要更新 chunk 的大小, 以反映合并后的 chunk 的大小. 并转到第10步.

  9. 若是执行到这一步, 说明释放了一个与 top chunk 相邻的chunk. 则不管它有多大, 都将它与 top chunk 合并, 并更新 top chunk 的大小等信息. 转下一步.

  10. 判断合并后的 chunk 的大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD, 若是是的话, 则会触发进行 fastbins 的合并操做, fastbins 中的 chunk 将被遍历, 并于相邻的空闲 chunk 进行合并, 合并后的 chunk 会被放到 bins 中. fastbins 将变为空, 操做完成以后转下一步.

  11. 判断 top chunk 的大小是否大于 DEFAULT_TRIM_THERESHOLD. 若是是的话, 则会试图归还 top chunk 中的一部分给操做系统. 可是最早分配的128KB的空间是不会归还. ptmalloc 会一直控制这部份内存. 用于响应用户的分配请求. 作完这一步以后, 释放结束, 从 free 函数退出.

参考文献

[1]   Doug Lea. A Memory Allocator. http://gee.cs.oswego.edu/dl/html/malloc.html.
[2]   Jonathan Bartlett. 内存管理内幕—动态分配的选择、折衷和实现. http://www-128.ibm.com/developerworks/cn/linux/l-memory/


* E-mail:phenixsen@gmail.com