原文:https://www.yuque.com/hxfqg9/bin/nvrgrs

什么是堆

在程序运行过程中,堆可以提供动态分配的内存,允许程序申请未知的内存。堆其实就是程序虚拟地址空间的一块连续线性区域,它由低地址往高地址增长。我们称管理堆的那部分程序为:堆管理器
堆管理器位于程序与内核之间,主要做:
1、响应用户申请内存的请求
2、管理用户所释放的内存
目前 Linux 标准发行版中使用的堆分配器是 glibc 的堆分配器:ptmalloc2,主要通过 mallo/free 来分配和释放内存块
不同的线程维护不同的堆称为:per thread arena
主线程创建的堆称为:main arena

glibc 的堆管理实现:
arena 指的是堆内存区域本身,并不是结构;主线程的 main arena 通过 sbrk 创建;其他线程的 arena 通过 mmap 创建
malloc_state 管理 arena 的核心结构,包含堆的状态信息、bins 链表等;main arena 对应的 malloc state 结构存储在 glibc 全局变量中;其他线程 arena 对应的 malloc_state 存储在 arena 本身中
bins 用来管理空闲内存块,通常用链表的结构来进行组织
chunks 内存块结构
在内存分配与使用中只有当真正去访问一个地址的时候,系统才会建立虚拟页面与物理内存的映射关系

堆相关知识【转载】 - 图1

Arena 头部结构:malloc_state 存储了 arena 的状态,其中的 bins[] 用于管理空闲块的 bins

  1. struct malloc_state
  2. {
  3. /* Serialize access. */
  4. mutex_t mutex;
  5. /* Flags (formerly in max_fast). */
  6. int flags;
  7. /* Fastbins */
  8. mfastbinptr fastbinsY[NFASTBINS];
  9. /* Base of the topmost chunk -- not otherwise kept in a bin */
  10. mchunkptr top;
  11. /* The remainder from the most recent split of a small request */
  12. mchunkptr last_remainder;
  13. /* Normal bins packed as described above */
  14. mchunkptr bins[NBINS * 2 - 2];
  15. /* Bitmap of bins */
  16. unsigned int binmap[BINMAPSIZE];
  17. /* Linked list */
  18. struct malloc_state *next;
  19. /* Linked list for free arenas. */
  20. struct malloc_state *next_free;
  21. /* Memory allocated from the system in this arena. */
  22. INTERNAL_SIZE_T system_mem;
  23. INTERNAL_SIZE_T max_system_mem;
  24. };

主要关心这么几个:
mfastbinptr fastbinsY[NFASTBINS],保存了 fastbins 各个链表的数组的头,大小为10 记录的是fast bin链
mchunkptr top,指向了 top chunk
mchunkptr bins[NBINS * 2 - 2],大小为129。记录的是unsorted bin(1)、small bin(2~63)、large bin链(64~126)

chunk的结构

我们称由 malloc 申请的内存为 chunk,这块内存在 ptmalloc 中被称为 malloc_chunk 结构体表示
无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同

  1. /*
  2. This struct declaration is misleading (but accurate and necessary).
  3. It declares a "view" into memory allowing access to necessary
  4. fields at known offsets from a given base. See explanation below.
  5. */
  6. struct malloc_chunk {
  7. INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
  8. INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
  9. struct malloc_chunk* fd; /* double links -- used only if free. */
  10. struct malloc_chunk* bk;
  11. /* Only used for large blocks: pointer to next larger size. */
  12. struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  13. struct malloc_chunk* bk_nextsize;
  14. };

prev_size:如果前一个 chunk 是空闲的话,记录物理相邻前一个 chunk 的大小;否则存储前一个的数据
size:该 chunk 的大小,必须是 2*SIZE_SZ 的整数倍,后三位分别是:

  • NON_MAIN_ARENA(A):表示该 chunk 属于主分配区(1)或者非主分配区(0)
  • IS_MAPPED(M):记录当前 chunk 是否是由 mmap 分配的,M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的
  • PREV_INUSE(P):记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。

fd、bk:chunk 处于分配时从 fd 字段开始就是用户数据了,chunk 空闲时 会被添加到对应的空闲管理链表中

  • fd:指向下一个(非物理相邻)空闲的 chunk
  • bk:指向上一个(非物理相邻)空闲的 chunk
  • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理

fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)

  • fd_nextsize:指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
  • bk_nextsize:指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。

一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

使用中的 chunk 结构

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处
chunk 指针指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域和相关的控制信息,图中 mem 指针才是真正返回给用户的内存指针。

堆相关知识【转载】 - 图2

堆相关知识【转载】 - 图3

空闲中的 chunk 结构

堆相关知识【转载】 - 图4

当 chunk 空闲时,其 M 状态不存在,只有 A P 原本是数据区的地方存储四个指针
指针 fd 指向前一个空闲的 chunk,而 bk 指向后一个空闲的 chunk,plmalloc 通过这两个指针将大小相近的 chunk 连成一个双向链表。
对于 large bin 中的空闲 chunk,还有两个指针 fd_nextsize 和 bk_nextsize,这两个指针用于加快 large bin 中查找最近匹配的空闲 chunk 。不同的 chunk 链表又是通过 bins 或者 fastbins 来组织
一般情况下,物理相邻的两个空闲 chunk 会被合并为一个 chunk 。堆管理器会通过 prev_size 字段以及 size 字段合并两个物理相邻的空闲 chunk 块
如果前一个 chunk 处于使用状态,那么不需要去通过链表串起来,所以当前 chunk 也就不需要 prev_size,当申请的内存大小对 2*size_t 取余之后比 size_t 小于等于的话就可以用它的下一个 chunk 的 prev_size
比如:64位下 malloc(0x45),那 0x45 mod 0x10 还差 0x5,那他就可以用后面一个 chunk 的 prev_size,最后加上 chunk header 大小是 0x50
还是 64 位下,如果大小是 0x49 的话取余之后还差 0x5,那就没法用了,只能多申请一块,最后加上 chunk header 用了 0x60

堆相关知识【转载】 - 图5

内存对齐

直接整理个表格

机器类型 64位 32位
对齐位数 16 8
size_t 8 4

bin

bin链表中指向的是 chunk header
ptmalloc 采用分箱式方法对空闲的 chunk 进行管理。首先,它会根据空闲的 chunk 的大小以及使用状态将 chunk 初步分为 4 类:
fast bins,用于管理较小的 chunk,在之前说的那个 fastbinY 数组里面
small bins,用于管理中等大小的 chunk
large bins,用于管理较大的 chunk
unsorted bin,用于存放未整理的 chunk
bin 对应的数据结构

  1. #define NBINS 128
  2. /* Normal bins packed as described above */
  3. mchunkptr bins[ NBINS * 2 - 2 ];

fastbin

单向链表后进先出,同时 p 位被保留防止合并,同一大小的 chunk 会在同一条链上,不同大小的 chunk 在不同的链上
不同平台大小不同,列一个索引,当 malloc 的大小在这个范围内的时候会首先去 fastbin 中找

fastbinsY[] x86(size_t=4) x64(size_t=8)
0 0x10 0x20
1 0x18 0x30
2 0x20 0x40
3 0x28 0x50
4 0x30 0x60
5 0x38 0x70
6 0x40 0x80

unsortedbin

双向循环链表,先进先出,存放所有不满足 fastbin,未被整理的 chunk

malloc 的时候在其他 bin 没找到合适的就会遍历 unsortedbin 同时根据大小放到对应的 bin 里

smallbin

双向链表,先进先出

释放的时候会检查相邻的是不是 free 的,如果是进行合并然后放到 unsortedbin

堆相关知识【转载】 - 图6

largebin

双向链表,先进先出

需要根据 fd_nextsize 和 bk_nextsize 指针从大到小排序

tcache

libc2.26 及以后有,先进后出,最大为0x410

每个 tcache bin 最多只能有 7 个,prev_inuse 不会置零,也就是不会进行合并,七个填满之后就跟之前一样了

指针直接指向 chunk 的 userdata 部分

malloc_consolidate功能

  1. 首先判断当前 malloc_state 结构体中的 fastbin 是否为空,如果为空就说明整个 malloc_state 都没有完成初始化,需要对malloc_state 进行初始化。
  2. malloc_state 的初始化操作由函数 malloc_init_state(av) 完成,该函数先初始化除 fastbins 之外的所有的bins,再初始化 fastbins,清空 fastbins

consolidate情景

1、当去 malloc 一个大于 smallbin 的 chunk 时

将 fastbin 中的 chunk 都整理到 unsortedbin 中,遇到相邻的就合并了,与 topchunk 相邻的就与 topchunk 合并

判断 unsortedbin 中有没有能直接拿来用的,如果有就拿来用,没有就看看有没有能切割的,有的话就切出来拿来用产生 last remainder,同时把 unsortedbin 中的 chunk 整理到对应的 bins 中去,都没有的话就从 topchunk 中找

以下内容复制自:https://blog.csdn.net/sinat_19596835/article/details/81665095

分配过程:

  1. ptmalloc在开始时,若请求的空间小于mmap分配阈值(mmap threshold,默认值为128KB)时,主分配区会调用sbrk()增加一块大小为 (128 KB + chunk-size) align 4KB的空间作为heap,若大于mmap分配阈值,则ptmalloc直接使用mmap()映射一块大小为chunk的内存作为heap。非主分配区会调用mmap映射一块大小为HEAP-MAX-SIZE(32位系统上默认为1MB,64位系统上默认为64MB)的空间作为sub-heap。当用户请求内存分配时,首先会在这个区域找一块合适的chunk给用户。当用户释放heap中的chunk时,ptmalloc又会使用fast bins和bins来组织空闲chunk。
  2. 若brk!=brk-start,若用户申请内存,先判断所需分配chunk的大小是否满足chunk-size<=max-fast(max-fast默认为64B),如果是的话则转到下一步。
  3. 首先尝试在fast bins中取一个所需大小的chunk分配给用户。如果可以找到,则分配结束。否则转到下一步。
  4. 判断所需大小是否在small bins中,即判断chunk-size < 512B是否成立。如果chunk大小处在small bins中,则转下一步,否则转6步
  5. 根据所需分配的chunk的大小,找到具体所在的某个small bin,从该bin的尾部摘取一个恰好满足大小的chunk。若成功,则分配结束,否则,转到下一步。
  6. 到了这一步,说明需要分配的内存较大。ptmalloc首先遍历fast bins中的chunk,并将相邻的chunk进行合并,并链接到unsorted bin中,然后遍历unsorted bin中的chunk,如果unsorted bin只有一个chunk,并且这个chunk在上次分配过程中被使用过,并且所需分配的chunk大小属于small bins,并且chunk的大小大于等于需要分配的大小,这种情况下就直接将该chunk进行切割,分配结束,否则将根据chunk的空间大小将其放入small bins或是large bins中,遍历完成后,转入下一步
  7. 到了这一步,说明需要分配的内存较大。或者small bins和unsorted bins中都找不到合适的chunk,并且fast bins和unsorted bins中所有的chunk都清除干净了。从large bins中按照“smallest-first,best-fit”原则,找一个合适的chunk,从中划分一块所需大小的chunk,并将剩下的部分链接回到bins中。若操作成功,则分配结束,否则转到下一步。
  8. 如果搜索fast bins和bins都没有找到合适的chunk,那么就需要操作top chunk来进行分配了。判断top chunk大小是否满足所需chunk的大小,如果是,则从top chunk中分出一块来。否则转到下一步。
  9. 到了这一步,说明top chunk也不能满足分配要求,所以,于是就有了两个选择: 如果是主分配区,调用sbrk(),增加top chunk大小;如果是非主分配区,调用mmap来分配一个新的sub-heap,增加top chunk大小;或者使用mmap()来直接分配。在这里,需要依靠chunk的大小来决定到底使用哪种方法。判断所需分配的chunk大小是否大于等于 mmap分配阈值,如果是的话,则转下一步,调用mmap分配,否则跳到第11步,增加top chunk 的大小。
  10. 使用mmap系统调用为程序的内存空间映射一块chunk_size align 4kB大小的空间。 然后将内存指针返回给用户。
  11. 判断是否为第一次调用malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB大小的空间作为初始的heap。若已经初始化过了,主分配区则调用sbrk()增加heap空间,非主分配区则在top chunk中切割出一个chunk,使之满足分配需求,并将内存指针返回给用户。

释放过程

  1. 判断传入的指针是否为0,如果为0,则什么都不做,直接return。否则转下一步。
  2. 判断所需释放的chunk是否为mmaped chunk,如果是,则调用munmap()释放mmaped chunk,解除内存空间映射,该该空间不再有效,访问该区域会报错。如果开启了mmap分配阈值的动态调整机制,并且当前回收的chunk大小大于mmap分配阈值,将mmap分配阈值设置为该chunk的大小,将mmap收缩阈值设定为mmap分配阈值的2倍(??没看懂为什么),释放完成,否则跳到下一步。
  3. 判断chunk的大小和所处的位置,若chunk_size <= max_fast,并且chunk并不位于heap的顶部,也就是说并不与top chunk相邻,则转到下一步,否则跳到第5步。(因为与top chunk相邻的小chunk也和 top chunk进行合并,所以这里不仅需要判断大小,还需要判断相邻情况)
  4. 将chunk放到fast bins中,chunk放入到fast bins中时,并不修改该chunk使用状态位P。也不与相邻的chunk进行合并。只是放进去,如此而已。这一步做完之后释放便结束了,程序从free()函数中返回。
  5. 判断前一个chunk是否处在使用中,如果前一个块也是空闲块,则合并。并转下一步。
  6. 判断当前释放chunk的下一个块是否为top chunk,如果是,则转第8步,否则转下一步。
  7. 判断下一个chunk是否处在使用中,如果下一个chunk也是空闲的,则合并,并将合并后的chunk放到unsorted bin中。注意,这里在合并的过程中,要更新chunk的大小,以反映合并后的chunk的大小。并转到第9步。
  8. 如果执行到这一步,说明释放了一个与top chunk相邻的chunk。则无论它有多大,都将它与top chunk合并,并更新top chunk的大小等信息。转下一步。
  9. 判断合并后的chunk 的大小是否大于FASTBIN-CONSOLIDATION-THRESHOLD(默认64KB),如果是的话,则会触发进行fast bins的合并操作,fast bins中的chunk将被遍历,并与相邻的空闲chunk进行合并,合并后的chunk会被放到unsorted bin中。fast bins将变为空,操作完成之后转下一步。
  10. 判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。判断top chunk的大小是否大于mmap收缩阈值(默认为128KB),如果是的话,对于主分配区,则会试图归还top chunk中的一部分给操作系统。但是最先分配的128KB空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配请求;如果为非主分配区,会进行sub-heap收缩,将top chunk的一部分返回给操作系统,如果top chunk为整个sub-heap,会把整个sub-heap还回给操作系统。做完这一步之后,释放结束,从 free() 函数退出。可以看出,收缩堆的条件是当前free的chunk大小加上前后能合并chunk的大小大于64k,并且要top chunk的大小要达到mmap收缩阈值,才有可能收缩堆。,ptmalloc 会一直管理这部分内存,用于响应用户的分配请求;如果为非主分配区,会进行sub-heap收缩,将top chunk的一部分返回给操作系统,如果top chunk为整个sub-heap,会把整个sub-heap还回给操作系统。做完这一步之后,释放结束,从 free() 函数退出。可以看出,收缩堆的条件是当前free的chunk大小加上前后能合并chunk的大小大于64k,并且要top chunk的大小要达到mmap收缩阈值,才有可能收缩堆。

呸呸呸,还是得 RTFSC(Read The Fucking Source Code)