glibc malloc 内存分配优化策略解读

概述

内存分配器ptmalloc,即glibc中的malloc,实现了 malloc(),free()以及一组其它的函数,以提供动态内存管理的支持。分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序。 为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。

内存管理结构

chunk

chunk是glibc内存管理的最小单位,其数据结构如下所示

在这里插入图片描述

在这里插入图片描述

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_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中几个关键的成员有prev_size、mchunk_size、fd和bk,其作用分别为:

prev_size:如果前一个chunk是free chunk,则这个内容保存的是前一个chunk的大小。 如果前一个chunk是allocated chunk,则这个区域保存的是前一个chunk的用户数据。

mchunk_size:当前chunk的大小。最后的 3 位作为标志位,具体为:

第0比特位用于表示前一个chunk是否为allocated chunk

第1比特位用于标记该chunk是否是通过系统调用申请的(子线程是mmap,主线程则是通过 brk)。如果是,则该chunk不再由内存管理数据结构来标记,申请释放流程将简化。

第2比特位用于标记该chunk是否属于主分配区,关于分配区将在下文详细介绍。

fd:前向指针,即指向当前chunk在同一个bin的下一个chunk的指针,仅chunk未使用的时候存在。

bk:后向指针,即指向当前chunk在同一个bin的上一个chunk的指针,仅chunk未使用的时候存在。

arena

arena一般称为分配区,是一个结构体,内含指向各自类型内存块的指针等元素,每个线程在申请内存时会获取一个。分配区分为主分配区和thread分配区,前者仅有一个,其余均为thread分配区。当新创建的线程需要申请内存时,将从一个全局的链表中获取一个空闲的分配区,如果没有得到且分配区数量没有超过最大值(M_ARENA_MAX),malloc将会新建一个。

具体逻辑可见下文 多线程下的竞争抢锁。

heap

heap包括帧头和内存块, glibc以heap为单位从操作系统批量申请和释放内存。 主分配区有一个heap,thread分配区在刚创建时也只有一个,当超过一定大小时会新增heap,heap直接以链表形式相连,数量没有限制,单个heap最大默认64M。新建heap时里面只有一个chunk,称为top chunk,每次申请内存时都会从top chunk中分裂出一块chunk,而top chunk本身则始终位于heap的末端。

下图是只有一个heap的main arena和thread arena的内存分布图:

在这里插入图片描述
下图是一个thread arena中含有多个heap的情况:

在这里插入图片描述

从以上两图可以看出,thread arena只含有一个arena,却有两个heap_info(即 heap header)。由于两个heap是通过mmap从操作系统申请的内存,两者在内存布局上并不相邻而是分属于不同的内存区间,所以为了便于管理,glibc的malloc将第二个heap_info结构体的prev成员指向了第一个heap_info结构体的起始位置(即ar_ptr成员),而第一个heap_info结构体的ar_ptr成员指向了arena,这样就构成了一个单链表,方便后续管理。

typedef struct _heap_info
{
  mstate ar_ptr; /* Arena for this heap. */
  struct _heap_info *prev; /* Previous heap. */
  size_t size;   /* Current size in bytes. */
  size_t mprotect_size; /* Size in bytes that has been mprotected
                           PROT_READ|PROT_WRITE.  */
  /* Make sure the following data is properly aligned, particularly
     that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
     MALLOC_ALIGNMENT. */
  char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

mstate ar_ptr:ar_ptr 是一个指向 arena 结构(mstate)的指针。arena 是用于动态内存分配的内存区域。这个指针将堆与其关联的 arena 连接起来。

struct _heap_info *prev:prev 是一个指向前一个 heap_info 结构体的指针,用于形成一个链表。这使得可以管理多个堆段,每个堆段通过链表连接起来。

size_t size:size 存储当前堆的大小,以字节为单位。这表示由该堆段管理的总内存量。

size_t mprotect_size:指示已使用 mprotect 保护的堆的大小(以字节为单位),该保护具有 PROT_READ 和 PROT_WRITE 标志。mprotect 是一个系统调用,用于控制内存区域的保护,例如使其可读和可写。

char pad:pad 是一个填充字段,用于确保 heap_info 结构体的正确对齐。填充的大小通过按位与计算 -6 * SIZE_SZ 和 MALLOC_ALIGN_MASK 得出。

内存管理链表

对于空闲的 chunk,ptmalloc 采用分箱式内存管理方式,根据空闲 chunk 的大小和处于的状态将其放在不同的 bin 中。

glibc使用的内存池如下图示,glibc提供了几种链表来管理不同大小的chunk。其中,除tcache外,其余均为arena结构体中的成员变量。

在这里插入图片描述

内存池保存在bins这个长128的数组中,每个元素都是一双向个链表。其中:

  • bins[0]目前没有使用
  • bins[1]的链表称为unsorted_list,用于维护free释放的chunk。
  • bins[2,63)的区间称为small_bins,用于维护<512字节的内存块,其中每个元素对应的链表中的chunk大小相同,均为index*8。
  • bins[64,127)称为large_bins,用于维护>512字节的内存块,每个元素对应的链表中的chunk大小不同,index越大,链表中chunk的内存大小相差越大,例如: 下标为64的chunk大小介于[512, 512+64),下标为95的chunk大小介于[2k+1,2k+512)。同一条链表上的chunk,按照从小到大的顺序排列。

tcache

tcache是glibc为了提升小块内存申请释放性能引入的缓存机制。单个tcache有64个链表项,每一项里面最多可保存7块大小相同的chunk,tcache链表本身的数据结构从分配区管理的heap中申请,线程退出时释放回原heap,由于tcache是线程变量,每个线程都会有一个自己的tcache,因此理论上数量无上限。

fastbin

fastbin为管理小块chunk(64位为160字节)的链表,应对频繁申请小块内存的场景。链表项管理的chunk值按一定规律递增,可通过一定的算法算出指定大小的chunk所在的链表项索引,从而找到对应大小的chunk。

在这里插入图片描述

tcache 是一个每个线程独有的缓存机制。这意味着每个线程都有自己的 tcache,避免了多线程竞争而fastbin 是一个全局缓存机制,不是线程私有的。这意味着多个线程可能会竞争访问 fastbin。

unsortedbin

fastbin中整合的chunk和small chunk、 large chunk free之后的chunk被放入unsortedbin,加速内存申请释放,unsortedbin管理的chunk值无规律。

在这里插入图片描述

smallbin、largebin

smallbin和largebin管理的chunk值按一定规律递增,可通过一定的算法算出指定大小的chunk所在的链表项索引,从而找到对应大小的chunk。

内存分配策略

申请流程

glibc中malloc内存分配大体逻辑:

  • 分配内存 < DEFAULT_MMAP_THRESHOLD,走__brk,从内存池获取,失败的话走brk系统调用
  • 分配内存 > DEFAULT_MMAP_THRESHOLD,走__mmap,直接调用mmap系统调用

其中,DEFAULT_MMAP_THRESHOLD默认为128k,可通过mallopt进行设置。 重点看下小块内存(size > DEFAULT_MMAP_THRESHOLD)的分配,

glibc在内存池中查找合适的chunk时(此处不考虑fastbin和tcache),采用了最佳适应的伙伴算法。

1、如果分配内存<512字节,则通过内存大小定位到smallbins对应的index上(floor(size/8))

  • smallbins[index]为空,进入步骤3
  • smallbins[index]非空,直接返回第一个chunk

2、如果分配内存>512字节,则定位到largebins对应的index上

  • largebins[index]为空,进入步骤3
  • largebins[index]非空,扫描链表,找到第一个大小最合适的chunk,如size=12.5K,则使用chunk B,剩下的0.5k放入unsorted_list中

3、遍历unsorted_list,查找合适size的chunk,如果找到则返回;否则,将这些chunk都归类放到smallbins和largebins里面

4、index++从更大的链表中查找,直到找到合适大小的chunk为止,找到后将chunk拆分,并将剩余的加入到unsorted_list中

5、如果还没有找到,那么使用top chunk

6、或者,内存<128k,使用brk;内存>128k,使用mmap获取新内存

在这里插入图片描述

释放流程

free释放内存到其内存池时,有两种情况:

  1. chunk和top chunk相邻,则和top chunk合并
  2. chunk和top chunk不相邻,则直接插入到unsorted_list

在这里插入图片描述

内存碎片

按照glibc的内存分配策略,我们考虑下如下场景(假设brk其实地址是512k):

  1. malloc 40k内存,即chunkA,brk = 512k + 40k = 552k
  2. malloc 50k内存,即chunkB,brk = 552k + 50k = 602k
  3. malloc 60k内存,即chunkC,brk = 602k + 60k = 662k
  4. free chunkA。

此时,由于brk = 662k,而释放的内存是位于[512k, 552k]之间,无法通过移动brk指针,将区域内内存交还操作系统,因此,在[512k, 552k]的区域内便形成了一个内存碎片。 按照glibc的策略,free后的chunkA区域由于不和top chunk相邻,因此,无法和top chunk 合并,应该挂在unsorted_list链表上。

多线程下的竞争抢锁

并发条件下,main_arena引发的竞争将会成为限制程序性能的瓶颈所在,因此glibc采用了多arena机制,线程A分配内存时获取main_arena锁成功,将在main_arena所管理的内存中分配;此时线程B获取main_arena失败,glibc会新建一个arena1,此次内存分配从arena1中进行。

这种策略,一定程度上解决了多线程下竞争的问题;但是随着arena的增多,内存碎片出现的可能性也变大了。例如,main_arena中有10k、20k的空闲内存,线程B要获取20k的空闲内存,但是获取main_arena锁失败,导致留下20k的碎片,降低了内存使用率。

普通arena结构

  1. 一个arena由多个Heap构成
  2. 每个Heap通过mmap获得,最大为1M,多个Heap间可能不相邻
  3. Heap之间有prev指针指向前一个Heap
  4. 最上面的Heap,也有top chunk

每个Heap里面也是由chunk组成,使用和main_arena完全相同的管理方式管理空闲chunk。

在这里插入图片描述

main arena和普通arena的区别 main_arena是为一个使用brk指针的arena,由于brk是堆顶指针,一个进程中只可能有一个,因此普通arena无法使用brk进行内存分配。普通arena建立在mmap的机制上,内存管理方式和main_arena类似,只有一点区别,普通arena只有在整个arena都空闲时,才会调用munmap把内存还给操作系统。

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值