ptmalloc底层原理剖析

目录

一、概述

二、基础了解

2.1 32位进程默认内存布局

2.2 brk & sbrk & mmap

三、内存管理

2.1 结构

2.1.1 main_arena 与 non_main_arena

2.1.2 malloc_chunk

2.1.3 空闲链表bins

2.1.4 初始化

2.2 内存分配与释放

2.3 使用注意

三、ptmalloc、tcmalloc与jemalloc实现机制对比分析


一、概述

ptmalloc是开源 GNU C Library (glibc) 默认的内存管理器,当前大部分Linux服务端程序使用的是ptmalloc提供的malloc/free系列函数,而其在性能上远差于Meta的jemalloc和Google的tcmalloc

服务端程序调用ptmalloc提供的malloc/free函数申请和释放内存,ptmalloc提供对内存的集中管理,以尽可能达到:

  • 用户申请和释放内存更加高效,避免多线程申请内存并发和加锁

  • 寻求与操作系统交互过程中内存占用和malloc/free性能消耗的平衡点,降低内存碎片化,不频繁调用系统调用函数

为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,并且ptmalloc会将已经使用的和空闲的内存管理起来;当用户需要释放内存free时,ptmalloc又会将回收的内存管理起来,根据实际情况决定是否回收给操作系统(内存池通性

二、基础了解

2.1 32位进程默认内存布局

栈至顶向下扩展,堆至底向上扩展,mmap映射区域至顶向下扩展。mmap映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域

2.2 brk & sbrk & mmap

int brk(const void *addr)
void* sbrk(intptr_t incr)
  • 两者的作用都是扩展heap的上界
  • brk()的参数设置为新的brk上界地址,成功返回1,失败返回0
  • sbrk()的参数为申请内存的大小,返回heap新的上界brk的地址
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset)
int munmap(void& addr, size_t length)
  • mmap第一种用法是映射此盘文件到内存中
  • mmap第二种用法是匿名映射,不映射此盘文件,而向映射区申请一块内存,malloc使用的是第二种用法
  • munmap用于释放内存

三、内存管理

2.1 结构

为了解决多线程锁争夺问题,将内存分配区分为主分配区 (main_area) 和非主分配区 (no_main_area)。同时,为了便于管理内存,对预申请的内存采用边界标记法划分成很多块 (chunk);ptmalloc内存分配器中,malloc_chunk是基本组织单元,用于管理(描述)不同类型的chunk,功能和大小相近的chunk串联成链表,被称为一个bin

2.1.1 main_arena 与 non_main_arena

内存分配器中,为了解决多线程锁争夺问题,分为主分配区main_area(分配区的本质就是内存池,管理着chunk)和非主分配区no_main_area

  • ​ 主分配区和非主分配区形成一个环形链表进行管理

  • ​ 每个分配区利用互斥锁使线程对于该分配区的访问互斥

  • ​ 每个进程只有一个主分配区,允许有多个非主分配区

  • ​ ptmalloc根据系统对分配区的调用动态增加分配区的大小,分配区的数量一旦增加,则不会减少

  • ​ 主分配区可以使用brk()和mmap()来分配,而非主分配区只能使用mmap()来映射内存块

  • ​ 申请小内存时会产生很多内存碎片,ptmalloc在整理时也需对分配区做加锁操作

​当一个线程需要使用malloc分配内存时,先查看该线程的私有变量中是否存在一个分配区,若是存在,会尝试对其进行加锁操作。若加锁成功,就会在使用该分配区分配内存;若是失败,就会遍历循环链表中获取一个未加锁的分配区。若是整个链表中都没有未加锁的分配区,则会开辟一个新的分配区,将其加入全局的循环链表并加锁,然后使用该分配区进行分配。当释放这块内存时,同样会先获取待释放内存块所在的分配区的锁。若是有其他线程正在使用该分配区,则必须等待其他线程释放该分配区互斥锁后才能进行内存释放

注意:

  • 非主分配区虽然是mmap()分配,但是和大于128K直接使用mmap()分配没有任何关系。大于128K的内存使用mmap()分配,使用完之后直接用ummap()还给系统

  • 每个线程在malloc会先获取一个area,使用area内存池分配各自的内存,存在竞争关系

  • 为了避免竞争,可以使用线程局部存储的策略,thead cache(tcmalloc中的tc正是此意)

2.1.2 malloc_chunk

ptmalloc统一管理heap和mmap映射区域中空闲的chunk,当用户进行分配请求时,会先试图在空闲的chunk中查找和分割,从而避免频繁的系统调用,降低内存分配的开销。为了更好的管理和查找空闲chunk,在预分配的空间的前后添加了必要的控制信息

  • prev_size: 若前一个chunk是空闲的,该域表示前一个chunk的大小;若不空闲,该域无意义(知道当前chunk地址,减去prev_size,便得到前一个chunk的地址,prev_size主要用于相邻空闲的chunk合并)
  • size:当前chunk的大小,并且记录了如下一些其他属性
    • 前一个chunk在使用中 (P = 1)
    • 当前chunk是mmap映射区域分配 (M = 1) 或是heap区域分配 (M = 0)
    • 当前chunk属于非主分配区 (A = 0) 或非主分配区 (A = 1)
  • fd 和 bk: 只有该chunk空闲时才会存在,其作用是用于将对应的空闲chunk块加入到空闲chunk块链表中统一管理,若该chunk块被分配给应用程序使用,那么这两个指针也就没有用,所以此区域也被当作应用程序的使用空间
  • fd_nextsize & bk_nextsize:当前chunk存在于large bins中,large bins中的空闲chunk是按照大小排序,若存在多个同一大小的chunk,增加这两个字段可以加快遍历空闲chunk,并查找满足需要的空闲chunk,fd_nextsize指向下一个比当前chunk大的第一个空闲chunk,bk_nextsize指向前一个比当前chunk小的第一个空闲chunk。若该chunk块被分配给应用程序使用,那么这两个指针也就没有用,所以此区域也被当作应用程序的使用空间
//ptmalloc源码中定义结构体malloc_chunk来描述这些块
struct malloc_chunk
{
  INTERNAL_SIZE_T      prev_size;    /* Size of previous chunk (if free).  */  
  INTERNAL_SIZE_T      size;         /* Size in bytes, including overhead. */  
  
  struct malloc_chunk* fd;           /* double links -- used only if free. */  
  struct malloc_chunk* bk;  
  
  /* Only used for large blocks: pointer to next larger size.  */  
  struct malloc_chunk* fd_nextsize;      /* double links -- used only if free. */  
  struct malloc_chunk* bk_nextsize; 
};

使用中的chunk

  • chunk指针指向chunk开始的地址;mem指针指向用户内存块开始的地址
  • ​ P=0时,表示前一个chunk为空闲,prev_size才有效
  • ​ P=1时,表示前一个chunk正在被使用,prev_size无效。p主要用于内存块的合并操作。ptmalloc分配的第一个块总是将p设为1,以防程序引用到不存在的区域
  • ​ M=1为mmap映射区域分配;M=0为heap区域分配
  • ​ A=0为主分配区分配;A=1为非主分配区分配

空闲的chunk

 chunk空闲时,M状态不存在,只有AP状态。因为M表示是由brk还是mmap分配的内存,而mmap分配的内存free时直接munmap,不会放到空闲链表。原本是用户数据区的地方存储了四个指针。指针fd指向后一个空闲chunk,而bk指向前一个空闲的chunk,malloc通过这两个指针将大小相近的chunk连成一个双向链表

chunk中的空间复用

  • 为了使chunk占用的空间最小,ptmalloc采用了空间复用。一个chunk在不同状态下,某些区域表现出来不同的意义,以此达到复用
  • 空闲时,一个chunk至少需要 2个size_t和2个指针 大小的空间,用来存储prev_size、size、fd和bk,即16bytes
  • 当一个chunk处于使用状态时,其下一个chunk的prev_size域肯定是无效的,所以这个空间也可以被当前chunk使用

所以,一个使用中的chunk的大小的计算公式为:in_use_size = (用户请求 + 8 - 4)

加8bytes是为了存储prev_size和size,但又因为向下一个chunk借了4bytes,所以减去4。因为空闲的chunk和使用中的chunk使用的是同一块空间,所以要取最大值作为实际的分配空间,即最终的分配空间为chunk_size = max(in_use_size,16)

特殊chunk

top chunk

  • ​top chunk相当于分配区的顶部空闲内存,当bins都不能满足内存分配要求时,就会来top chunk上分配
  • ​当top chunk大小比用户所请求大小还大时,top chunk会分为两个部分,user chunk和remainder chunk(剩余大小)。其中remainder chunk成为新的top chunk
  • ​当top chunk大小小于用户所请求的大小时,top chunk就通过sbrk(main arena)或mmap(thread arena)系统调用来扩容

mmaped chunk

  • ​当分配的内存非常大(大于分配阈值,默认128k)时,需要被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存时会直接交还给操作系统(chunk中M标志位为1)

last remainder chunk

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

2.1.3 空闲链表bins

当用户free内存,ptmalloc并不会马上将内存交还给操作系统,而是被ptmalloc的空闲链表bins管理起来,当下次进程需要malloc内存时,ptmalloc就会从空闲的bins上寻找一块合适的内存块分配给用户使用,避免频繁的系统调用,降低内存分配的开销

​ptmalloc将相似大小的chunk用双向链表连接起来,这样一个链表被称为一个bin,ptmalloc共维护了128个bin,每个bin都维护了大小相近的双向链表的chunk。基于chunk的大小,有下列几种可用bins:

 注意:32位平台下,bin[0]与bin[127]不存在。bin[1]为unsorted bins,bin[2]~bin[126]为sorted bins

unsorted bin

  • unsorted bins的队列位于bins数组的第2个(下标为1),是bins的一个缓冲区,加快分配的速度
  • 当用户释放的内存大于max_fast或者fast bins合并后的chunk都会首先进入unsorted bins,chunk大小无限制,任何大小chunk都可以进入。这种途径给予ptmalloc第二次机会重新使用最近free的chunk,寻找合适的bin的时间开销就省略了,因此分配和释放更快
  • ​用户malloc时,若fast bins没有找到合适的chunks,则malloc会先在unsorted bin中查找合适的空闲chunk。若没有合适的bin,ptmalloc会将unsorted bin上的chunk放入bins上,然后在bins上查找合适的空闲chunk

small bins

  • 小于512bytes的chunk被称为small chunk,而保存small chunk的bin被称为small bin。下标从2开始,到63结束,共62个。small bin每个bin之间相差8 bytes,同一个small bin的chunk具有相同大小
  • ​每个small bin都包括一个空闲区块的双向循环链表,free掉的chunk添加在链表的前端,而所需chunk则从链表后端摘除
  • ​两个相连的空闲chunk会被合并成一个空闲chunk,合并消除了碎片化的影响但是减慢了free的速度
  • ​分配时,当small bin非空,相应的bin会摘除binlist中最后一个chunk并返回用户。在free一个chunk时,检查其前或其后的chunk是否空闲,若是则合并,即将chunk从所属的链表中摘除合并成一个新的chunk,新的chunk会添加在unsorted bin链表前端

large bins

  • 大于512bytes的chunk被称为large chunk,而保存为large chunk的bin被称为large bin,位于small bins后面。下标从64开始,到126结束,共63个。large bins中的每个bin分别包含了一个给定范围内的chunk,其中的chunk按大小递减排序,大小相同则按照最近使用时间排列
  • ​两个相邻的空闲chunk会被合并成一个空闲chunk
  • ​分配时,遵循"smallest-first,best-fit",从顶部遍历到底部以找到一个大小最接近用户需求的chunk。一旦找到,相应chunk就会分成两块,user chunk(用户请求大小)返回给用户,remainder chunk剩余部分添加到unsorted bin
  • free和small bin类似

fast bins

程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的chunk后,也许马上就会有另一个小块内存的请求,分配器又需从大的空闲内存中切分出一块,较为低效,故引入fast bins

  • fast bins是bins的高速缓冲区,大约有10个定长队列(bin)。每个fast bin都记录着一条free chunk的单链表(binlist,采用单链表是因为fast bin中链表的chunk不会被摘除的特点),增删chunk都发生在链表的前端
  • ​当用户释放一块不大于max_fast(默认值为64bytes)的chunk时,会默认放到fast bins上。当需要给用户分配的chunk小于等于max_fast时,malloc首先会到fast bins上寻找是否有合适的chunk。一定大小内的chunk无论是分配还是释放,都会在fast bins中过一遍。
  • ​分配时,binlist中被检索的第一个chunk将被摘除并返回给用户,free掉的chunk将被添加在索引到的binlist前端

2.1.4 初始化

  • ​在堆区中,start_brk指向heap的开始,而brk指向heap顶部。可以使用brk() & sbrk()来增加分配给用户的heap空间。在使用malloc前,brk的值等于start_brk,即heap大小=0
  • ​ptmalloc在开始时,若请求的空间小于mmap分配阈值(默认为128KB)时,主分配区会调用sbrk()增加一块大小为(128KB + chunk_size)的空间作为heap,非主分配区会调用mmap映射一块大小为HEAP_MAX_SIZE(32位系统默认为1MB,64位系统默认为64MB)的空间作为sub-heap
  • ​当用户请求内存分配时,首先会在这个区域找一块合适的chunk给用户,当用户释放了heap中的chunk时,ptmalloc又会使用fast bins和bins来组织空闲chunk
  • ​若需要分配的chunk大小小于mmap分配阈值,而heap空间又不够,则此时主分配区会通过sbrk()调用来增加heap大小,非主分配区会调用mmap映射一块新的sub-heap,即增加top chunk的大小,每次heap增加的值都会对齐到4KB
  • 当用户的请求超过mmap分配阈值,并且主分配区使用sbrk()分配失败的时候,或是非主分配区在top chunk中不能分配到需要的内存时,ptmalloc会尝试使用mmap()直接映射一块内存到进程内存空间。使用mmap()直接映射的chunk在释放时直接结束映射,不再属于进程的内存空间。任何对该内存的访问都会产生段错误。而在heap中或是sub-heap中分配的空间则可能会留在进程内存空间内,还可再次引用

2.2 内存分配与释放

内存分配malloc流程

1、获取分配区的锁,防止多线程冲突(每个进程有一个malloc管理器,而一个进程中的多个线程共享这一个管理器,有竞争)

2、计算出需要分配的内存的chunk实际大小

3、若chunk 的大小 < max_fast(64bytes),在fast bins上查找适合的chunk;若不存在,转到 5

4、若chunk 大小 < 512bytes,从small bins上去查找chunk,若存在,分配结束

5、需要分配的是一块大的内存,或者 small bins 中找不到 chunk:

  • a. 遍历fast bins,合并相邻的chunk,并链接到unsorted bin中
  • b. 遍历unsorted bin中的chunk:
    • ①能够切割chunk直接分配,分配结束
    • ②根据chunk的空间大小将其放入small bins或是large bins中,遍历完成后,转到6

6、需要分配的是一块大的内存,或者small bins和unsorted bin中都找不到合适的 chunk,且fast bins和unsorted bin中所有的chunk已清除:

  • 从large bins中查找,遍历链表,直到找到第一个大小大于待分配的chunk进行切割,余下放入unsorted bin,分配结束

7、检索fast bins和 bins都没有找到合适的chunk,判断top chunk大小是否满足所需chunk的大小,从top chunk中分配

8、top chunk不能满足需求,需扩大 top chunk:

  • 当top chunk大小大于用户请求时,top chunk会分为两部分:User chunk和remainder chunk,其中remainder chunk成为新的top chunk
  • 当top chunk大小小于用户请求时,top chunk就通过sbrk()或者mmap()系统调用来扩容

9、top chunk也不能满足分配要求时,若是主分配区,调用sbrk()增加top chunk大小;若是非主分配区,调用mmap来分配一个新的sub-heap,增加top chunk大小;或者使用mmap()来直接分配:

  • 若所需分配的chunk大于等于mmap分配阈值,使用mmap系统调用为程序的内存空间映射一块chunk_size align 4KB大小的空间。然后将内存指针返回给用户
  • 若所需分配的chunk小于等于mmap分配阈值,判断是否为第一次调用malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的heap。若已经初始化过了,主分配区则调用sbrk()增加heap空间,非主分配区则在top chunk中切割出一个chunk,使之满足分配需求,并将内存指针返回给用户

内存释放 free 流程

1、获取分配区的锁

2、若free 的是空指针,返回

3、若当前chunk是mmap映射区域映射的内存,调用munmap () 释放内存

4、若chunk与top chunk相邻,直接与top chunk合并,转到 8

5、若chunk 的大小 > max_fast,放入unsorted bin,并且检查是否有合并:

  • a. 没有合并情况则free
  • b. 有合并情况并且和top chunk相邻,转到 8

6、若chunk 的大小 < max_fast,放入fast bin,并且检查是否有合并:

  • a.fast bin并没有改变chunk的状态,没有合并情况则free
  • b. 有合并情况,转到 7

7、在fast bins,若相邻chunk空闲,则将这两个chunk合并,放入unsorted bin。若合并后的大小 > 64KB,会触发进行fast bins的合并操作,fast bins中的chunk将被遍历合并,合并后的chunk会被放到unsorted bin中。合并后的chunk和top chunk相邻,则会合并到top chunk中,转到8

8、若top chunk的大小 > mmap收缩阈值(默认为 128KB),对于主分配区,会试图归还top chunk中的一部分给操作系统

2.3 使用注意

为了避免Glibc内存暴增,需要注意:

  1. 后分配的内存先释放,因为ptmalloc收缩内存是从top chunk开始,若与top chunk相邻的chunk不能释放,top chunk以下的chunk都无法释放
  2. ptmalloc不适合用于管理长生命周期的内存,特别是持续不定期分配和释放长生命周期的内存,这将导致ptmalloc内存暴增
  3. 多线程分阶段执行的程序不适合用ptmalloc,这种程序的内存更适合用内存池管理
  4. 尽量减少程序的线程数量和避免频繁分配、释放内存。频繁分配,会导致锁的竞争,最终导致非主分配区增加,内存碎片增高,且性能降低
  5. 防止内存泄露,ptmalloc对内存泄露是相当敏感的,根据其内存收缩机制,若与top chunk相邻的那个chunk没有回收,将导致top chunk一下很多的空闲内存都无法返回给操作系统
  6. 防止程序分配过多内存,或是由于Glibc内存暴增,导致系统内存耗尽,程序因OOM被系统杀掉。预估程序可以使用的最大物理内存大小,配置系统的/proc/sys/vm/overcommit_memory,/proc/sys/vm/overcommit_ratio,以及使用ulimt –v限制程序能使用虚拟内存空间大小,防止程序因OOM被杀掉

三、ptmalloc、tcmalloc与jemalloc实现机制对比分析

ptmalloc(glibc malloc):

  • ptmalloc是GNU C库(glibc)中的默认内存分配器,广泛用于Linux系统
  • 基于Doug Lea的malloc实现,采用了多种技术,如自由链表、分离器和堆的延迟绑定等
  • ptmalloc的特点是成熟、稳定,并且与GNU C库紧密集成

tcmalloc(Google malloc):

  • tcmalloc是Google开发的内存分配器,主要用于Google的C++代码
  • tcmalloc通过减少锁的竞争和减少内存碎片来提高性能
  • 使用线程本地缓存(Thread-Caching Malloc)的概念,将内存分配的任务分散到不同的线程中,以减少对共享数据结构的竞争
  • tcmalloc还有其他一些优化策略,如小对象合并、高效的分配器缓存等

jemalloc:

  • jemalloc是一款通用的内存分配器,由FreeBSD社区开发,并逐渐被其他系统广泛采用
  • jemalloc致力于提供高度可扩展性和低碎片化的内存分配
  • 使用了多个技术,如分离的内存区域、伙伴分配器、线程本地缓存等
  • jemalloc还提供了高级特性,如背景线程执行释放、空间利用统计和分析等

性能对比:

  • ptmalloc在大多数情况下性能良好,但在多线程环境下可能存在一些竞争问题
  • tcmalloc通过线程本地缓存和减少锁竞争,适用于高并发场景,尤其是多线程服务器应用
  • jemalloc在可扩展性和碎片化方面表现出色,特别适用于大型内存分配和高负载场景

总结:

  • ptmalloc适用于常规应用,与GNU C库集成紧密
  • tcmalloc适用于高并发多线程环境,通过线程本地缓存减少竞争
  • jemalloc适用于可扩展性和低碎片化要求高的场景,提供高级特性和统计信息

猜你喜欢

转载自blog.csdn.net/GG_Bruse/article/details/131742920
今日推荐