ptmalloc底层原理剖析

目录

一、概述

二、基础了解

2.1 32位进程默认内存布局

2.2 brk & sbrk & mmap

三、内存管理

2.1 结构

2.1.1 main_area 与 non_main_area

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_area 与 non_main_area

内存分配器中,为了解决多线程锁争夺问题,分为主分配区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适用于可扩展性和低碎片化要求高的场景,提供高级特性和统计信息

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
本文通过Glibc的内存暴增问题,主要介绍了系统的内存管理问题,具体如下: 目录 1. 问题 2. 基础知识 2.1 X86平台Linux进程内存布局 2.1.1 32位模式下进程内存经典布局 2.1.2 32位模式下进程默认内存布局 2.1.3 64位模式下进程内存布局 2.2 操作系统内存分配的相关函数 2.2.1 Heap操作相关函数 2.2.2 Mmap映射区域操作相关函数 3. 概述 3.1 内存管理一般性描述 3.1.1 内存管理的方法 3.1.2 内存管理器的设计目标 3.1.3 常见C内存管理程序 3.2 Ptmalloc内存管理概述 3.2.1 简介 3.2.2 内存管理的设计假设 3.2.3 内存管理数据结构概述 3.2.4 内存分配概述 3.2.5 内存回收概述 3.2.6 配置选项概述 3.2.7 使用注意事项 4. 问题分析及解决 5. 源代码分析 5.1 边界标记法 5.2 分箱式内存管理 5.2.1 Small bins 5.2.2 Large bins 5.2.3 Unsorted bin 5.2.4 Fast bins 5.3 核心结构体分析 5.3.1 malloc_state 5.3.2 Malloc_par 5.3.3 分配区的初始化 5.4 配置选项 5.5 Ptmalloc的初始化 5.5.1 Ptmalloc未初始化时分配/释放内存 5.5.2 ptmalloc_init()函数 5.5.3 ptmalloc_lock_all(),ptmalloc_unlock_all(),ptmalloc_unlock_all2() 5.6 多分配区支持 5.6.1 Heap_info 5.6.2 获取分配区 5.6.3 Arena_get2() 5.6.4 _int_new_arena() 5.6.5 New_heap() 5.6.6 get_free_list()和reused_arena() 5.6.7 grow_heap(),shrink_heap(),delete_heap(),heap_trim() 5.7 内存分配malloc 5.7.1 public_mALLOc() 5.7.2 _int_malloc() 5.8 内存释放free 5.8.1 Public_fREe() 5.8.2 _int_free() 5.8.3 sYSTRIm()和munmap_chunk(

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GG_Bond20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值