首先,让我们来介绍两个概念:main arena与no main arena。这两个术语翻译过来,分别对应的是“主分配区”和“非主分配区”。
那么,主分配区和非主分配区各自具有哪些特点呢?
首先,需要明确的是,系统中仅存在一个主分配区。在进行内存分配时,我们必须对主分配区进行加锁操作,以确保内存分配的安全性,待分配结束后,再解除锁。
主分配区可以通过sbrk和mmap两种方式向操作系统请求虚拟内存。而相比之下,非主分配区则只能访问通过mmap映射得到的内存区域。此外,每当非主分配区使用mmap申请内存时,它都会默认以“HEAP MAX SIZE”作为分配的内存空间大小。
并且系统通常仅在必要时才会选择使用mmap来申请内存。
使用中的chunk结构
p = 0代表pre_chuank处于空闲状态
p = 1代表pre_chuank使用中
用来M表示是不是mmap分配的内存空间,1是,0不是
A表示是否为主分配区1是main arena,0是no main arena
char name = malloc(8);name指向Mem指针 ;name-2就是Chunk指针指向的地方
空闲中的chunk结构
标志位没有M标志位,A,P表示和使用中的chunk相同
fd指向该chunk向后的一个chunkbk指向向前的一个空闲chunk
fd_next size,bk_next size只会出现在large bin中
chunk的空间复用
我们需要知道的是,为了避免空间的浪费.使得我们chunk所占的空间最小,ptmalloc采用了空间复用 这一技术
32位中最小的min_chunk是16b
pre
chunk 一个4b
fd
bk
无论我们申请的空间多小,都会分配16b
64位bit同理
空闲的chunk容器
源码:
#define NBINS 128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define MIN_LARGE_SIZE (NSMALLBINS * SMALLBIN_WIDTH)
#define in_smallbin_range(sz) \
((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE)
#define smallbin_index(sz) \
(SMALLBIN_WIDTH == 16 ? (((unsigned)(sz)) >> 4) : (((unsigned)(sz)) >> 3))
#define largebin_index_32(sz) \
(((((unsigned long)(sz)) >> 6) <= 38)? 56 + (((unsigned long)(sz)) >> 6): \ ((((unsigned long)(sz)) >> 9) <= 20)? 91 + (((unsigned long)(sz)) >> 9): \ ((((unsigned long)(sz)) >> 12) <= 10)? 110 + (((unsigned long)(sz)) >> 12): \ ((((unsigned long)(sz)) >> 15) <= 4)? 119 + (((unsigned long)(sz)) >> 15): \ ((((unsigned long)(sz)) >> 18) <= 2)? 124 + (((unsigned long)(sz)) >> 18): \ 126)
// XXX It remains to be seen whether it is good to keep the widths of
// XXX the buckets the same or whether it should be scaled by a factor
// XXX of two as well.
#define largebin_index_64(sz) \
(((((unsigned long)(sz)) >> 6) <= 48)? 48 + (((unsigned long)(sz)) >> 6): \ ((((unsigned long)(sz)) >> 9) <= 20)? 91 + (((unsigned long)(sz)) >> 9): \ ((((unsigned long)(sz)) >> 12) <= 10)? 110 + (((unsigned long)(sz)) >> 12): \ ((((unsigned long)(sz)) >> 15) <= 4)? 119 + (((unsigned long)(sz)) >> 15): \ ((((unsigned long)(sz)) >> 18) <= 2)? 124 + (((unsigned long)(sz)) >> 18): \ 126)
#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64 (sz) : largebin_index_32 (sz))
#define bin_index(sz) \
((in_smallbin_range(sz)) ? smallbin_index(sz) : largebin_index(sz))
bin_index(sz)宏根据所需内存大小sz来计算并确定合适的bin的索引。若所需内存大小落在small bins的范围内,则调用smallbin_index(sz)来计算索引;若超出此范围,则调用largebin_index(sz)来确定索引。
smallbin_index(sz)的计算方法相对直接:在SIZE_SZ(即size字段的大小)为4字节的情况下,将sz除以8;若SIZE_SZ为8字节,则将sz除以16。这里的除数实际上是small bins中等差数列的公差。
相比之下,largebin_index(sz)的计算过程则稍显复杂。为了更直观地理解chunk的大小范围与bin索引之间的关系,我们可以参考以下表格(以SIZE_SZ为4字节的平台为例):
注意:重要的是要理解上表所展示的是chunk大小与bin index之间的对应关系。当用户请求分配特定大小的内存(记为req)时,首先需要checked_request2size(req, sz)函数来计算满足该请求所需的chunk大小(记为sz)。这一步骤至关重要,因为它确保了请求的内存大小被适当地转换为内存分配器内部所使用的chunk大小。随后,利用bin_index(sz)宏,我们可以根据已计算出的chunk大小sz来确定该chunk所属的bin的索引。这一过程是内存分配策略中的关键一环,它决定了如何从可用的内存bins中高效地选择和分配内存。
bins
为了提升内存分配与释放的效率,我们并不会立即将释放的内存归还给操作系统。相反,ptmalloc(一种内存分配器)会负责统一管理heap(堆内存)和通过mmap映射得到的内存区域中的空闲chunk(内存块)。当接收到新的内存分配请求时,ptmalloc会优先尝试从这些空闲的chunk中挑选一块合适的来满足用户需求,这样做可以有效减少系统调用的频率,进而降低内存分配的整体开销。ptmalloc采用了一种高效的管理策略,即将大小相近的chunk通过双向链表链接起来,形成所谓的“bin”。这样的设计使得内存管理更加有序和高效。值得注意的是,ptmalloc总共维护了128个这样的bin,并使用一个数组来高效地存储和访问这些bins。通过这种方式,ptmalloc能够快速地响应内存分配请求,同时保持内存管理的灵活性和效率。
这些bins在内存管理中起到了类似缓存(cache)的作用。它们暂存了不同大小的空闲内存块(chunks),当程序需要分配内存时,ptmalloc会首先在这些bins中查找是否有合适大小的空闲内存块可用。如果找到了,就可以直接将其分配给程序,而无需向操作系统申请新的内存。这样,就减少了系统调用的次数,提高了内存分配的效率。就像缓存会存储经常访问的数据以减少访问主存的延迟一样,这些bins也存储了常用的、不同大小的内存块,以加速内存分配的过程。当内存块被释放时,它们会被放回相应的bins中,以便后续的内存分配请求可以重用这些内存块。
并且系统为了进一步提升速度,还建立了fastbin这个特殊的容器
程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的chunk之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,ptmalloc中在分配过程中引入了fast bins,不大子max fast(32位中为0x80)的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中。
unsorted Bin
如果被用户释放的chunk大于max fast,或者fast bins中的空闲chunk合并后,这些chunk首先会被放到unsorted bin队列中
在进行malloc操作的时候,如果在fastbins中没有找到合适的chunk,那么ptmal0c会先在unsorted bin中查找合适的空闲chunk,然后才查找bins。
如果我们的unsorted bin不能满足分配要求,malloc便会将unsorted bin中的chunk加入对应的bins中,然后再从bins中继续进行查找和分配过程。
unsorted bin可以看做是bins的一个缓冲区,增加它只是为了加快分配的速度,
三个特殊chunk
有三个chunk机制比较特殊,这里我们也来说一下,即:
top chunk
mmaped chunk
last remainder
top chunk
概念:当一个chunk处于一个arena的最顶部(即最高内存地址处)的时候,就称之为top chunk。
作用:该chunk并不属于任何bin,而是在系统当前的所有free chunk(无论那种bin)都无法满足用户请求的内存大小的时候,将此chunk当做一个应急消防员,分配给用户使用。
分配的规则:如果top chunk的大小比用户请求的大小要大的话,就将该top chunk分作两部分:1)用户请求的chunk;2)剩余的部分成为新的top chunk。否则,就需要扩展heap或分配新的heap了——在main arena中通过sbrk扩展heap,而在thread arena中通过mmap分配新的heap。
mmaped chunk
如果top chunk也不能满足要求,那么ptmalloc就会使用mmap直接去将页映射到内存空间,这个chunk在被free的时候直接解除映射。
last remainder
如果在bins链中存在freechunk时,当我们去malloc的时候,malloc的请求大小比freechunk的大小小,那么arena就会切割这个freechunk给malloc使用,那么切割之后剩余的chunk就被称为“last remainder”。
当产生last remainder之后,表示arena的malloc_state结构体中的last_remainder成员指针就会被初始化,并且指向这个last remainder。
sbrk和mmap
当otmaloc开始的时候,如果我们申请的内存小于我们mmap的分配國值(32位128kb),我们的主分配区就会用sbrk来增加一大块内存作为heap,而非主分配区会使用mmap来映射一块空间作为sub-heap
而当我们的chunk<mmap的分配阈值,但我们的hep已经不够分配时,如果是主分配区,就会用sbrk来增加大小,而非主分配区则会新映射一块内存
而当chunk>mmap分配阈值的时候,并且主分配区使用 sbrK0)分配失败的时候,或是非主分配区在 top chunk 中不能分配到需要的内存时,就会用mmap来映射内存
而当我们回收chunk时,如果chunk>mmap的分配阈值,系统就会把分配阈值调整为当前回收的chunk大小,然后将我们的mmap分配阈值设为之前的两倍
注意分配阈值是否为动态。
内存分配的具体步骤
首先,系统会获取目标分配区的锁,这是为了确保内存分配过程的线程安全,防止多个线程同时访问并修改同一个分配区的数据,从而引发数据竞争和不一致性。如果系统发现目标分配区未被锁定,则会立即上锁,以此保证当前线程对分配区的独占访问权。若所有现有的分配区均已被锁定,系统会考虑开辟新的分配区以满足需求。
一旦拥有了专属的分配区(注意,用户请求的内存大小与系统实际分配的内存大小可能不同),系统会根据用户请求的内存大小来进行后续的分配操作。首先,系统会检查请求的内存大小是否落在fastbin的范围内(即是否小于max_fast)。若满足条件,系统会尝试在fastbin中寻找大小合适的空闲内存块(chunk)。一旦找到,便立即返回给用户。
若fastbin中无满足条件的chunk,系统会进一步判断请求的内存大小是否适合smallbin(即是否小于small bin的阈值)。若适合,系统会按照相应的大小在smallbin中进行查找。如果找到,就从smallbin的尾部摘取一个chunk返回给用户。若仍未找到,系统会继续在其他bins中搜索。
若请求的内存大小既不适合fastbin也不适合smallbin,这可能意味着用户需要一块较大的内存。此时,系统会首先遍历fastbin,尝试合并相邻的chunk,并将它们链接到unsortedbin中。然后,系统会检查unsortedbin中的chunk。如果unsortedbin中仅有一个chunk,且其大小大于用户请求的大小但符合smallbin的规格(且若该chunk过大,则会进行切割以满足用户需求),则系统会进行相应的切割并返回给用户。否则,系统会根据chunk的大小将其放入smallbin或largebin中。
若smallbin和unsortedbin均无法满足需求,且fastbin和unsortedbin中的chunk已被清空,这意味着用户可能需要一块非常大的内存。此时,系统会遍历largebin以寻找合适的chunk。
若遍历largebin后仍未找到满足条件的chunk,系统会考虑使用top chunk。首先,系统会检查top chunk的大小是否足够。若足够,则从中分割出相应大小的内存块返回给用户。若top chunk的大小也不足,系统会判断当前是否在主分配区。在主分配区时,系统会调用sbrk()函数来增大top chunk的大小;在非主分配区时,则会调用mmap来增加top chunk的大小或直接分配内存。此时,系统还需判断所需内存大小是否超过mmap的分配阈值。若超过,则直接映射一段内存区域并返回给用户;若未超过且是首次调用malloc,则主分配区会先分配一块区域作为heap。若已初始化过heap,主分配区会通过brk()函数增加heap的大小;而非主分配区则会在top chunk中切割出一个chunk返回给用户。
内存回收的具体步骤
在内存回收的过程中,确保线程安全同样至关重要,因此,我们首先需要获取分配区的锁。这一步骤是内存回收操作的基础,它能够有效防止多个线程同时操作同一个分配区,从而避免数据竞争和内存管理混乱的问题。
接下来,我们会检查传入的指针是否为空。如果指针为空,即没有指向任何有效的内存块,那么回收操作就没有实际意义,我们会直接返回,不做任何处理。
如果指针不为空,我们会进一步判断该内存块是否是通过mmap函数分配的。mmap函数通常用于分配较大的内存块,并且这些内存块与堆内存的管理是分开的。如果确认是通过mmap分配的,我们会调用munmap函数来解除该内存块的空间映射。此时,如果再次尝试访问该内存块,系统将会报告段错误,因为该内存块已经不再属于进程的地址空间。
如果内存块不是通过mmap分配的,我们会根据其大小进行分类处理。对于小于max_fast的内存块,如果它并不位于堆的顶端,即不与top chunk相邻,我们会将其放入fastbins中,并且不会修改其P标志位(即pre_in_use标志),这表示该内存块在回收前是处于使用状态的。
对于大于max_fast的内存块,我们会检查其前一个内存块的状态。如果前一个内存块也是空闲的,我们会将它们合并成一个更大的内存块。然后,我们会判断合并后的内存块是否与top chunk相邻。如果相邻,我们会将合并后的内存块与top chunk再次合并,以充分利用空间。
如果合并后的内存块不与top chunk相邻,我们会检查下一个内存块是否也是空闲的。如果下一个内存块也是空闲的,我们会将它们合并成一个更大的内存块,并将合并后的内存块放入unsortedbin中。此时,我们会更新合并后内存块的大小,以确保内存管理的准确性。
在处理fastbins中的内存块时,我们还需要注意一个特殊情况。如果合并后的内存块大小超过了FASTBIN CONSOLIDATION THRESHOLD(即fastbin合并阈值),我们会触发fastbin的合并操作。此时,我们会遍历fastbins中的内存块,将它们与相邻的内存块合并,并将合并后的内存块链入unsortedbin中。这个过程中,fastbins会变空,因为它们中的内存块都已经被合并并放入了unsortedbin中。
最后,我们会检查top chunk的大小是否超过了mmap的收缩阈值。如果超过了,在主分配区中,我们会将top chunk的一部分内存返回给系统,但是初始化时分配的大小堆是不会被返回的。在非主分配区中,我们会收缩sub-heap,将top chunk的内存归还给系统。如果此时top chunk的大小正好等于整个sub-heap的大小,我们会将整个sub-heap都归还给操作系统,以释放不必要的内存资源。
bins结构
ptmalloc采用small bins来高效地管理那些较小的空闲内存块(chunk)。在这些small bins中,每个bin所管理的chunk大小与该bin的索引号之间存在着特定的数学关系:在SIZE_SZ为4字节的平台上,chunk的大小是8字节的等差数列,起始于16字节,递增至504字节,共涵盖了62个不同的bin。这些bin分别对应着16B、24B、32B,一直到504B的内存块大小。
而当SIZE_SZ为8字节时,情况则有所不同。此时,chunk的大小变为16字节的等差数列,从32字节起始,递增至1008字节,同样包含了62个bin。这些bin所对应的内存块大小依次为32B、48B、64B,直至1008B。
ptmalloc为了更有效地管理这些内存块,为每一个small bin都维护了一个双向环形链表。这些链表不仅拥有头节点,以便于对链表内的节点进行统一处理,从而简化了编程工作,而且确保了每个链表中存储的空闲chunk都具有相同的大小。这种设计使得当应用程序需要分配特定大小的内存空间时,可以直接从对应的链表中获取所需大小的chunk,既满足了应用程序的内存需求,又最大限度地减少了内存碎片的产生。
在SIZE_SZ为4字节的平台上,ptmalloc对512字节以下的空闲chunk采用了所谓的“分箱机制”进行组织。这种机制通过62个不同的small bin,将各种大小的空闲chunk分门别类地存储起来,形成了一个清晰、有序的内存管理结构。这种结构不仅提高了内存分配的效率,还有助于减少内存碎片,从而提升了整个系统的性能和稳定性。
(相同大小的chunk会连成一条)
本文章主要是在2.23及之前的libc版本,因为在更新2.26之后,glibc新增了tcache机制,这个tcache的优先级是比fastbin还要高的但其安全性极低,更容易利用,也就意味着在有tcache的系统中都是牺牲了安全性来追求速度的 因此,在glibc2.29版本中对其安全性进行了加固,此时的tcache就变得较难利用了。在此不做赘述。