ptmalloc堆内存管理机制(主要讨论Linux x86下32位系统)
在用户空间内(32位下为3G的空间),除过禁止访问空间(128M)、代码段、数据段、.bss段、栈、命令行参数及环境变量所占空间之外的空间都可以用做堆区,堆区的空间需要用户动态申请,那就不得不先介绍两个系统调用:brk()或者sbrk()和mmap()。
brk()函数的C语言形式声明:
void brk ( void end_data_segment);
brk()函数的作用实际上就是设置进程数据段的结束地址。即它可以扩大或缩小数据段。如果将数据段的结束地址向高地址移动,那么扩大的那部分就可以当做堆区使用。
mmap()函数:
它的作用就是向操作系统申请一块虚拟地址空间,当然这块虚拟地址空间可以映射到到某个文件(这也是这个系统调用的最初作用),当它不映射到某个文件时,我们称它为匿名空间,匿名空间便可以拿来作为堆空间。声明如下:
void *mmap ( void *start, size_t length, int prot, int flags,int fd, off_t offset);
mmap的前两个参数是用来设置需要申请空间的起始地址和长度的,如果起始地址设置为0,那么Linux系统会自动挑选合适的起始地址。
prot / flags 用来设置申请的空间的权限(可读、可写、可执行)和映射类型(文件映射、匿名空间)。
最后两个参数是用于文件映射时指定文件描述符和文件偏移的,这里不关心它们。
下来开始介绍ptmalloc:
ptmallloc是Doug Lea malloc的一个扩展版本,支持多线程。pamalloc实现了malloc ,free以及其他一组函数,以提供动态存管理的支持。其可分为以下几个层次:
1. arena
每一个进程只有一个主分配区(main arena),但可能存在多个非主分配区(non arena),ptmalloc 根据系统对分配区的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少了。
下面分别介绍主分配区和非主分配区:
主分配区:可以访问进程的heap区和mmap映射区域,也就是说主分配区可以使用 sbrk() 和 mmap()向操作系统申请虚拟内存。如果主分配区的内存是通过 mmap()向系统分配的,当 free 该内存时,主分配区会直接调用 munmap()将该内存归还给系统。
非主分配区:只能访问进程的 mmap 映射区域, 非主分配区每
次使用 mmap()向操作系统“批发” HEAP_MAX_SIZE(32 位系统上默认为 1MB, 64 位系统默认为 64MB) 大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售”出去,毕竟系统调用是相对低效的,直接从用户空间分配内存快多了。所以 ptmalloc 在必要的情况下才会调用 mmap()函数向操作系统申请虚拟内存。
2. chunk
不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示。接下来介绍一下chunk的格式。
一个正在使用的chunk格式:
chunk 指针指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域和相关的控制信息。图中的 mem 指针才是真正返回给用户的内存指针。
chunk 的第二个域的最低一位为 P,它表示前一个块是否在使用中, P 为 0 则表示前一个 chunk 为空闲,这时chunk 的第一个域 prev_size 才有效, prev_size 表示前一个 chunk 的 size,程序可以使用这个值来找到前一个 chunk 的开始地址。当 P 为 1 时,表示前一个 chunk 正在使用中, prev_size无效(这时prev_size区域可以被上一个chunk借用,提高复用率),程序也就不可以得到前一个 chunk的大小。不能对前一个 chunk进行任何操作。ptmalloc分配的第一个块总是将 P 设为 1,以防止程序引用到不存在的区域。
Chunk 的第二个域的倒数第二个位为 M,他表示当前 chunk 是从哪个内存区域获得的虚拟内存。 M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的。
Chunk 的第二个域倒数第三个位为 A, 表示该 chunk 属于主分配区或者非主分配区,如果属于非主分配区,将该位置为 1,否则置为 0。
- 1
- 2
- 3
- 4
- 5
一个空闲的chunk格式如下:
当 chunk 空闲时, 其 M 状态不存在,只有 AP 状态, 原本是用户数据区的地方存储了四个指针,指针 fd 指向后一个空闲的 chunk,而 bk 指向前一个空闲的 chunk, ptmalloc 通过这两个指针将大小相近的 chunk 连成一个双向链表(双向链表被称为bin)。 对于 large bin 中的空闲 chunk,还有两个指针,fd_nextsize 和 bk_nextsize,这两个指针用于加快在 large bin 中查找最近匹配的空闲chunk。 不同的 chunk 链表又是通过 bins 或者 fastbins 来组织的(bins 和 fastbins 在 后面介绍)。
- 1
- 2
一个最小的chunk为16B,即prev_size, size, fd 和 bk。
3. bin(chunk容器)
ptmalloc将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。 Ptmalloc 一共维护了 128 个 bin,并使用一个数组来存储这些 bin,这个数组被成为bin数组。
bin数组结构如下:
在32位平台下,bin[0] 和 bin[127] 是不存在的,因为最小的 chunk 为 16B, small bin 一共 62 个, large bin 一共 63 个,算上 bin[1] 加起来一共 128 个 bin。
其中 small bin 可以看成是公差为8的等差数列,其相邻两个 bin 的 chunk 的大小相差8B,如上图所示。large bin 的每个 bin 中的 chunk 大小不是一个固定的等差数列,而是分成六组,每组 bin 是一个固定公差的等差数列,每组的 bin 数量依次为 32、 16、 8、 4、 2、 1,公差依次为 64B、 512B、4096B、32768B、 262144B 等。
正是基于 bin 数组的这种关系,当用户给出一个需要申请的空间大小,ptmalloc 会很容易的确定改大小空间在哪个 bin 里。
下面介绍两个bin:fast bin 和 unsort bin 的管理机制。
fast bin:
不大于 max_fast(默认值为 64B)的 chunk 被释放后,首先会被放到 fast bin 中, fast bin 中的 chunk 并不改变它的使用标志 P。 这样也就无法将它们合并, 当需要给用户分配的 chunk 小于或等于 max_fast 时, ptmalloc 首先会在 fast bin 中查找相应的空闲块,然后才会去查找 bin 中的空闲 chunk。在某个特定的时候(在内存释放合并 top chunk 时,如果合并后的 chunk 的大小大于 FASTBIN_CONSOLIDATION_THRESHOLD(默认64KB), 会触发进行 fast bins 的合并操作),ptmalloc会遍历 fast bin中的 chunk,18将相邻的空闲 chunk 进行合并, 并将合并后的 chunk 加入 unsorted bin 中,然后再将 usorted bin 里的 chunk 加入 bins 中。
unsort bin:
如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后, 这些 chunk 首先会被放到 unsorted bin 队列中, 在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则 ptmalloc 会先在 unsortedbin 中查找合适的空闲chunk, 然后才查找 bins。 如果 unsorted bin 不能满足分配要求。 malloc便会将 unsorted bin 中的 chunk 加入 bins 中。 然后再从 bins 中继续进行查找和分配过程。 从这个过程可以看出来, unsorted bin 可以看做是 bins 的一个缓冲区, 增加它只是为了加快分配的速度。
并不是所有的 chunk 都按照上面的方式来组织,实际上,有三种例外情况。 top chunk,mmaped chunk 和 last remainder。
top chunk:
对于非主分配区会预先从 mmap 区域分配一块较大的空闲内存模拟 sub-heap, 通过管理 sub-heap 来响应用户的需求, 因为内存是按地址从低向高进行分配的, 在空闲内存的最高处, 必然存在着一块空闲chunk, 叫做 top chunk。 当 bins 和 fast bins 都不能满足分配需要的时候,ptmalloc 会设法在 top chunk 中分出一块内存给用户,如果 top chunk 本身不够大,分配程序会重新分配一个 sub-heap,并将 top chunk 迁移到新的 sub-heap 上,新的 sub-heap与已有的 sub-heap 用单向链表连接起来,然后在新的 top chunk 上分配所需的内存以满足分配的需要。 Top chunk 的大小是随着分配和回收不停变换的,如果从 top chunk 分配内存会导致 top chunk 减小,如果回收的 chunk 恰好
与 top chunk 相邻,那么这两个 chunk 就会合并成新的 top chunk,从而使 top chunk 变大。如果在 free 时回收的内存大于某个阈值, 并且 top chunk 的大小也超过了收缩阈值, ptmalloc会收缩 sub-heap,如果 top-chunk 包含了整个 sub-heap, ptmalloc 会调用 munmap 把整个sub-heap 的内存返回给操作系统。对于主分配区, 可以通过 sbrk()来增大或是收缩进程 heap 的大小, ptmalloc 在开始时会预先分配一块较大的空闲内存(也就是所谓的 heap), 主分配区的 top chunk 在第一次调用 malloc 时会分配一块(chunk_size + 128KB)align 4KB 大小的空间作为初始的 heap, 用户从 top chunk 分配内存时,可以直接取出一块内存给用户。在回收内存时, 回收的内存恰好与 top chunk 相邻则合并成新的 top chunk,当该次回收的空闲内存大小达到某个阈值, 并且 top chunk 的大小也超过了收缩阈值, 会执行内存收缩,减小 top chunk 的大小, 但至少要保留一个页大小的空闲内存, 从而把内存归还给操作系统。 如果向主分配区的 top chunk 申请内存, 而 top chunk 中没有空闲内存, ptmalloc会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。
mmaped chunk:
当需要分配的 chunk 足够大, 而且 fast bins 和 bins 都不能满足要求, 甚至 top chunk 本身也不能满足分配需求时, ptmalloc 会使用 mmap 来直接使用内存映射来将页映射到进程空间。 这样分配的 chunk 在被 free 时将直接解除映射, 于是就将内存归还给了操作系统, 再次对这样的内存区的引用将导致 segmentation fault 错误。 这样的 chunk 也不会包含在任何bin 中。
last remainder:
Last remainder 是另外一种特殊的 chunk,就像 top chunk 和 mmaped chunk 一样,不会在任何 bins 中找到这种 chunk。当需要分配一个 small chunk,但在 small bins 中找不到合适的 chunk,如果 last remainder chunk 的大小大于所需的 small chunk 大小,lastremainder chunk被分裂成两个 chunk,其中一个 chunk 返回给用户,另一个 chunk 变成新的 last remainder chuk。
内存分配概述
分配算法概述,以 32 系统为例, 64 位系统类似。
小于等于 64 字节:用 pool 算法分配。
64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。
大于等于 512 字节:用最佳匹配算法分配。
大于等于 mmap 分配阈值(默认值 128KB): 根据设置的 mmap 的分配策略进行分配,如果没有开启 mmap 分配阈值的动态调整机制,大于等于 128KB 就直接调用 mmap20分配。 否则,大于等于 mmap 分配阈值时才直接调用 mmap()分配。ptmalloc 的响应用户内存分配要求的流程如下图所示:
内存回收概述
free() 函数接受一个指向分配区域的指针作为参数,释放该指针所指向的 chunk。而具体的释放方法则看该 chunk 所处的位置和该 chunk 的大小。 free()函数的工作流程如下图所示: