百度工程师带你探秘 C++ 内存管理(ptmalloc 篇)
作者 | 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/wiki/MallocInternals
[2] https://sploitfun.wordpress.com/tag/ptmalloc/
[3] https://www.cnblogs.com/biterror/p/691323
推荐阅读【技术加油站】系列:
版权声明: 本文为 InfoQ 作者【百度Geek说】的原创文章。
原文链接:【http://xie.infoq.cn/article/b44b753bc94a026c618f67c5d】。文章转载请联系作者。
评论