Glibc内存管理--ptmalloc2源代码分析(五)

3.2.3内存管理数据结构概述

3.2.3.1  Main_arenanon_main_arena

Doug Lea实现的内存分配器中只有一个主分配区(main arena),每次分配内存都必须对主分配区加锁,分配完成后释放锁,在SMP多线程环境下,对主分配区的锁的争用很激烈,严重影响了malloc的分配效率。于是Wolfram GlogerDoug Lea的基础上改进使得Glibcmalloc可以支持多线程,增加了非主分配区(non main arena)支持,主分配区非主分配区用环形链表进行管理。每一个分配区利用互斥锁(mutex)使线程对于该分配区的访问互斥。

         每个进程只有一个主分配区,但可能存在多个非主分配区,ptmalloc根据系统对分配区的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少了。主分配区可以访问进程的heap区域和mmap映射区域,也就是说主分配区可以使用sbrkmmap向操作系统申请虚拟内存。而非主分配区只能访问进程的mmap映射区域,非主分配区每次使用mmap()向操作系统“批发”HEAP_MAX_SIZE32位系统上默认为1MB64位系统默认为64MB)大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售”出去,毕竟系统调用是相对低效的,直接从用户空间分配内存快多了。所以ptmalloc在必要的情况下才会调用mmap()函数向操作系统申请虚拟内存。

主分配区可以访问heap区域,如果用户不调用brk()或是sbrk()函数,分配程序就可以保证分配到连续的虚拟地址空间,因为每个进程只有一个主分配区使用sbrk()分配heap区域的虚拟内存。内核对brk的实现可以看着是mmap的一个精简版,相对高效一些。如果主分配区的内存是通过mmap()向系统分配的,当free该内存时,主分配区会直接调用munmap()将该内存归还给系统。

当某一线程需要调用malloc()分配内存空间时,该线程先查看线程私有变量中是否已经存在一个分配区,如果存在,尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果失败,该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经加锁,那么malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。

申请小块内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。每个加锁操作大概需要510cpu指令,而且程序线程很多的情况下,锁等待的时间就会延长,导致malloc性能下降。一次加锁操作需要消耗100ns左右,正是锁的缘故,导致ptmalloc在多线程竞争情况下性能远远落后于tcmalloc。最新版的ptmalloc对锁进行了优化,加入了PER_THREADATOMIC_FASTBINS优化,但默认编译不会启用该优化,这两个对锁的优化应该能够提升多线程内存的分配的效率。

3.2.3.2  chunk的组织

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

 

1Chunk格式

ptmalloc 在给用户分配的空间的前后加上了一些控制信息,用这样的方法来记录分配的信息,以便完成分配和释放工作。一个使用中的chunk(使用中,就是指还没有被free掉)在内存中的样子如图所示:

Glibc内存管理--ptmalloc2源代码分析(五)_第1张图片

 

在图中,chunk指针指向一个chunk的开始,一个chunk中包含了用户请求的内存区域和相关的控制信息。图中的mem指针才是真正返回给用户的内存指针。chunk的第二个域的最低一位为P,它表示前一个块是否在使用中,P0则表示前一个chunk为空闲,这时 chunk的第一个域prev_size才有效,prev_size表示前一个chunksize,程序可以使用这个值来找到前一个chunk的开始地址。当P1时,表示前一个chunk正在使用中,prev_size无效,程序也就不可以得到前一个chunk的大小。不能对前一个chunk进行任何操作。ptmalloc分配的第一个块总是将P设为1,以防止程序引用到不存在的区域。

Chunk的第二个域的倒数第二个位为M,他表示当前chunk是从哪个内存区域获得的虚拟内存。M1表示该chunk是从mmap映射区域分配的,否则是从heap区域分配的。

Chunk的第二个域倒数第三个位为A,表示该chunk属于主分配区或者非主分配区,如果属于非主分配区,将该位置为1,否则置为0

 

空闲chunk在内存中的结构如图所示:

Glibc内存管理--ptmalloc2源代码分析(五)_第2张图片

 

chunk空闲时,其M状态不存在,只有AP状态,原本是用户数据区的地方存储了四个指针,指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunkptmalloc通过这两个指针将大小相近的chunk连成一个双向链表。对于large bin中的空闲chunk,还有两个指针,fd_nextsizebk_nextsize,这两个指针用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的(binsfastbins3.2.3.3中介绍)。

 

2chunk中的空间复用

为了使得 chunk 所占用的空间最小,ptmalloc使用了空间复用,一个 chunk或者正在被使用,或者已经被free掉,所以chunk的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。以32位系统为例,空闲时,一个chunk中至少需要4size_t4B)大小的空间,用来存储prev_sizesizefdbk (见上图),也就是16Bchunk的大小要对齐到8B。当一个chunk处于使用状态时,它的下一个chunkprev_size域肯定是无效的。所以实际上,这个空间也可以被当前chunk使用。这听起来有点不可思议,但确实是合理空间复用的例子。故而实际上,一个使用中的chunk的大小的计算公式应该是:in_use_size = (用户请求大小+ 8 - 4 ) align to 8B,这里加8是因为需要存储prev_sizesize,但又因为向下一个chunk“借”了4B,所以要减去4。最后,因为空闲的chunk 和使用中的chunk使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间chunk_size = max(in_use_size, 16)。这就是当用户请求内存分配时,ptmalloc 实际需要分配的内存大小,在后面的介绍中。如果不是特别指明的地方,指的都是这个经过转换的实际需要分配的内存大小,而不是用户请求的内存分配大小。

3.2.3.3  空闲chunk容器

1Bins

用户free掉的内存并不是都会马上归还给系统,ptmalloc会统一管理heapmmap映射区域中的空闲的chunk,当用户进行下一次分配请求时,ptmalloc会首先试图在空闲的chunk中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。ptmalloc将相似大小的chunk用双向链表链接起来,这样的一个链表被称为一个binPtmalloc一共维护了128bin,并使用一个数组来存储这些bin(如下图所示)。

Glibc内存管理--ptmalloc2源代码分析(五)_第3张图片 

 

数组中的第一个为unsorted bin,数组中从2开始编号的前64bin称为small bins,同一个small bin中的chunk具有相同的大小。两个相邻的small bin中的chunk大小相差8bytessmall bins中的chunk按照最近使用顺序进行排列,最后释放的chunk被链接到链表的头部,而申请chunk是从链表尾部开始,这样,每一个chunk 都有相同的机会被ptmalloc选中。Small bins后面的bin被称作large binslarge bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小序排列。相同大小的chunk同样按照最近使用顺序排列。ptmalloc使用“smallest-firstbest-fit”原则在空闲large bins中查找合适的chunk

当空闲的chunk被链接到bin中的时候,ptmalloc会把表示该chunk是否处于使用中的标志P设为0(注意,这个标志实际上处在下一个chunk中),同时ptmalloc还会检查它前后的chunk是否也是空闲的,如果是的话,ptmalloc会首先把它们合并为一个大的chunk,然后将合并后的chunk放到unstored bin中。要注意的是,并不是所有的chunk被释放后就立即被放到bin中。ptmalloc为了提高分配的速度,会把一些小的的chunk先放到一个叫做fast bins的容器内。

 

2Fast Bins

一般的情况是,程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的chunk之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,ptmalloc中在分配过程中引入了fast bins,不大于max_fast (默认值为64B)的chunk被释放后,首先会被放到fast bins 中,fast bins中的chunk并不改变它的使用标志P。这样也就无法将它们合并,当需要给用户分配的chunk小于或等于max_fast时,ptmalloc首先会在fast bins中查找相应的空闲块,然后才会去查找bins中的空闲chunk。在某个特定的时候,ptmalloc会遍历fast bins中的chunk,将相邻的空闲chunk进行合并,并将合并后的chunk加入unsorted bin中,然后再将usorted bin里的chunk加入bins中。

 

3Unsorted Bin

unsorted bin的队列使用bins数组的第一个,如果被用户释放的chunk大于max_fast,或者fast bins中的空闲chunk合并后,这些chunk首先会被放到unsorted bin队列中,在进行malloc操作的时候,如果在fast bins中没有找到合适的chunk,则ptmalloc会先在unsorted bin中查找合适的空闲chunk,然后才查找bins。如果unsorted bin不能满足分配要求。malloc便会将unsorted bin中的chunk加入bins中。然后再从bins中继续进行查找和分配过程。从这个过程可以看出来,unsorted bin可以看做是bins的一个缓冲区,增加它只是为了加快分配的速度。

 

4Top chunk

并不是所有的chunk都按照上面的方式来组织,实际上,有三种例外情况。Top chunkmmaped chunklast remainder,下面会分别介绍这三类特殊的chunktop chunk对于主分配区和非主分配区是不一样的。

对于非主分配区会预先从mmap区域分配一块较大的空闲内存模拟sub-heap,通过管理sub-heap来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲chunk,叫做top chunk。当binsfast bins都不能满足分配需要的时候,ptmalloc会设法在top chunk中分出一块内存给用户,如果top chunk本身不够大,分配程序会重新分配一个sub-heap,并将top chunk迁移到新的sub-heap上,新的sub-heap与已有的sub-heap用单向链表连接起来,然后在新的top chunk上分配所需的内存以满足分配的需要,实际上,top chunk在分配时总是在fast binsbins之后被考虑,所以,不论top chunk有多大,它都不会被放到fast bins或者是bins中。Top chunk的大小是随着分配和回收不停变换的,如果从top chunk分配内存会导致top chunk减小,如果回收的chunk恰好与top chunk相邻,那么这两个chunk就会合并成新的top chunk,从而使top chunk变大。如果在free时回收的内存大于某个阈值,并且top chunk的大小也超过了收缩阈值,ptmalloc会收缩sub-heap,如果top-chunk包含了整个sub-heapptmalloc会调用munmap把整个sub-heap的内存返回给操作系统。

由于主分配区是唯一能够映射进程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的大小。

 

5mmaped chunk

当需要分配的chunk足够大,而且fast binsbins都不能满足要求,甚至top chunk本身也不能满足分配需求时,ptmalloc会使用mmap来直接使用内存映射来将页映射到进程空间。这样分配的chunk在被free时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致segmentation fault错误。这样的chunk也不会包含在任何bin中。

 

6Last remainder

         Last remainder是另外一种特殊的chunk,就像top chunkmmaped chunk一样,不会在任何bins中找到这种chunk。当需要分配一个small chunk,但在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需的small chunk大小,last remainder chunk被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chuk

你可能感兴趣的:(thread,多线程,数据结构)