作者 | daydreamer

前篇《探秘C++内存治理(实践篇)》次要介绍了Linux C++程序内存治理的实践根底,本文作为系列文章《探秘C++内存治理》的第二篇,将会探讨经典内存管理器ptmalloc如何治理C++程序的内存。借助分析ptmalloc解决问题的着重点和设计实现老本的衡量,更具体的出现c++内存治理面临的问题和工程落地中的巧思。

一、概述

ptmalloc是开源GNU C Library(glibc)默认的内存管理器,以后大部分Linux服务端程序应用的是ptmalloc提供的malloc/free系列函数,而它在性能上远差于Meta的jemalloc和Google的tcmalloc。服务端程序调用ptmalloc提供的malloc/free函数申请和开释内存,ptmalloc提供对内存的集中管理,以尽可能达到:

  • 用户申请和开释内存更加高效,防止多线程申请内存并发和加锁
  • 寻求与操作系统交互过程中内存占用和malloc/free性能耗费的平衡点,升高内存碎片化,不频繁调用零碎调用函数

简略概括ptmalloc的内存管理策略:

  • 事后向操作系统申请并持有一块内存供用户malloc,同时治理已应用和闲暇的内存
  • 用户执行free,会将回收的内存治理起来,并执行管理策略决定是否交还给操作系统

接下来,将从ptmalloc数据结构、内存调配及优缺点介绍最经典的c++内存管理器的实现和应用(以32位机为例)。

二、内存治理

2.1 数据结构

为了解决多线程锁抢夺问题,将内存调配辨别为主调配区(main\_area)和非主调配区(no\_main\_area)。同时,为了便于管理内存,对预申请的内存采纳边界标记法划分成很多块(chunk);ptmalloc内存分配器中,malloc\_chunk是根本组织单元,用于治理不同类型的chunk,性能和大小相近的chunk串联成链表,被称为一个bin。

main\_arena与non\_main\_arena

主调配区和非主调配区造成一个环形链表进行治理, 每一个调配区利用互斥锁实现线程对该调配区的拜访互斥。每个过程只有一个主调配区,但容许有多个非主调配区,且非主调配区的数量只减少不缩小。主调配区能够拜访过程的heap区域和mmap映射区域,即主调配区能够应用sbrk()和mmap()分配内存;非主调配区只能应用mmap()分配内存。

对于不同arena的管理策略大抵如下:

  • 分配内存
  • 查看该线程的公有变量中是否曾经存在一个调配区并对其进行加锁操作,如果加锁胜利,则应用该调配区分配内存;如果未找到该分区或加锁失败,遍历环形链表中获取一个未加锁的调配区
  • 如果整个环形链表中没有未加锁的调配区,开拓一个新的调配区,将其退出循环链表并加锁,应用该调配区满足以后线程的内存调配
  • 开释内存
  • 先获取待开释内存块所在的调配区的锁,如果有其余线程正在应用该调配区,期待其余线程开释该调配区互斥锁后,再开释内存

主调配区和非主调配区的构造如下:

其中fastbinsY和bins是对理论内存块的治理和操作构造:

  • fastbinsY: 用以保留fast bins
  • bins[NBINS * 2 - 2]: unsorted bin(1个,bin[1])、small bins(62 个,bin[2]~bin[63])、large bins(63 个,bin[64]~bin[126])的汇合,一共有 126 个表项(NBINS = 128),bin[0] 和 bin[127] 没有被应用

malloc\_chunk与bins

ptmalloc对立治理heap和mmap映射区域中闲暇的chunk,当用户进行调配申请时,会先试图在闲暇的chunk中查找和宰割,从而防止频繁的零碎调用,升高内存调配的开销。为了更好的治理和查找闲暇chunk,在预调配的空间的前后增加了必要的管制信息,内存治理构造malloc\_chunk的成员及作用如下:

  • mchunk_prev_size: 前一个闲暇chunk的大小
  • mchunk_size: 以后chunk的大小
  • 必要的属性标记位:
  • 前一个chunk在应用中(P = 1)
  • 以后chunk是mmap映射区域调配(M = 1)或是heap区域调配(M = 0)
  • 以后chunk属于非主调配区(A = 0)或非主调配区(A = 1)
  • fd和bk: chunk块闲暇时存在,用于将闲暇chunk块退出到闲暇chunk块链表中对立治理

基于chunk的大小和应用办法,划分出以下几种bins:

  • fast bins

    fast bins仅保留很小的堆,采纳单链表串联,增删chunk都产生在链表的头部,进一步提高小内存的调配效率。fast bins记录着大小以8字节递增的bin链表,个别不会和其余堆块合并。

  • unsorted bin

    small bins和large bins的缓冲区,用于放慢调配的速度,chunk大小无尺寸限度,用户开释的堆块,会先进入unsorted bin。调配堆块时,会优先查看unsorted bin链表中是否存在适合的堆块,并进行切割并返回。

  • small bins

    保留大小 < 512B的chunk的bin被称为small bins。small bins每个bin之间相差8个字节,同一个small bin中的chunk具备雷同大小,采纳双向循环链表串联。

  • large bins

    保留大小 >= 512B的chunk的bin被称为large bins。large bins中的每一个bin别离蕴含了一个给定范畴内的chunk,其中的chunk按大小降序,雷同大小按工夫降序。

当然,并不是所有chunk都按上述的形式来组织,其余罕用的chunk,如:

  • top chunk: 调配区的顶部闲暇内存,当bins不能满足内存调配要求的时候,会尝试在top chunk调配。
  • 当top chunk > 用户申请大小,top chunk会分为两个局部:用户申请大小(user chunk)和残余top chunk大小(remainder chunk)
  • 当top chunk < 用户所申请大小,top chunk就通过sbrk(main_arena)或mmap(non_main_arena)零碎调用来扩容

2.2 内存调配与开释

概括内存malloc和free的流程大抵如下:

内存调配malloc流程

1、获取调配区的锁

2、计算出须要调配的内存的chunk理论大小

3、如果chunk的大小 < max\_fast,在fast bins上查找适宜的chunk;如果不存在,转到5

4、如果chunk大小 < 512B,从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:

  • a.主分区上,如果调配的内存 < 调配阈值(默认128KB),应用brk()调配;如果调配的内存 > 调配阈值,应用mmap调配
  • b.非主分区上,应用mmap来调配一块内存

内存开释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 bin,如果相邻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中的一部分给操作系统

三、优缺点

ptmalloc作为glibc默认的内存管理器,曾经宽泛的满足大多数大型项目的内存治理,同时它的实现思路也对起初的内存管理器提供了借鉴。

ptmalloc的介绍暂告一段落,接下来的几篇文章将持续探讨高性能内存治理库的集大成者——jemalloc、tcmalloc内存治理库。

---------- END ----------

参考资料

[1] https://sourceware.org/glibc/...

[2] https://sploitfun.wordpress.c...

[3] https://www.cnblogs.com/biter...

举荐浏览【技术加油站】系列

百度工程师教你玩转设计模式(适配器模式)

揭秘百度智能测试在测试评估畛域实际

百度工程师带你探秘C++内存治理(实践篇)

从零到一理解APP速度测评

百度工程师教你玩转设计模式(工厂模式)