堆管理器:Ptmalloc2 (HEAP MANAGER)
HEAP MANGER
为什么要有堆管理器
堆是动态分配的虚拟内存(只有在程序访问到这块虚拟内存时,系统才会建立虚拟内存和物理内存的映射,称为延迟绑定机制,大大节省了内存),从低地址向高地址生长,程序可能需要频繁的向操作系统申请内存的系统调用,与此同时就伴随这从用户态到内核态的多次转换,转换的同时需要保存context,影响程序性能,堆管理器就出现了,在操作系统和用户当作中间人,他会申请较大的内存,管理分配,1、响应用户申请内存的请求2、管理用户所释放的内存,堆管理器并非由内核实现的,Linux标准发行版中使用的堆分配器是glibc中的堆分配器:ptmalloc2
平台:操作系统 --> 堆管理器 --> 用户 |
堆管理器ptmalloc2
arena 指的是管理器在第一次malloc时申请到的大内存快,堆内存区域本身,并不是结构,可以理解为内存池;主线程的 main arena通过==(s)brk==创建,没开alsr分布在数据区(.data/.bss)的结尾,开启后在尾部随机偏移处;其他线程的 arena包括一些主线程的大内存 是通过 ==mmap== 创建,分布在Memory-mapped segment,也就是我们glic动态链接库加载的区域
malloc_state(main arena 的 malloc_state 并不是 heap segment 的一部分,而是一个全局变量,存储在 libc.so 的数据段)管理 arena 的核心结构,包含堆的状态信息、bins 链表等;main arena 对应的 malloc state 结构存储在 glibc 全局变量中;其他线程 arena 对应的 malloc_state 存储在 arena 本身中
bin 用来管理空闲内存块,通常用链表的结构来进行组织
chunks 内存块结构,是malloc具体在arena上申请给用户的
chunks
top chunk arena中从未被使用过的内存区域,inuse 比特位始终为 1,否则会合并旁边的chunk
malloc_chunk 已被分配且填写了相应数据的chunk
free chunk 被释放掉的malloced chunk
allocated chunk
是一个更加通用的术语,它可以指任何已经被分配给应用程序使用的内存块。
last remainder chunk malloc分割原chunk后剩余的部分
/* |
prev_size
如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一chunk 大小)是空闲的话,那该字段记录的是前一个chunk 的大小 (包括chunk头)。否则,该字段可以用来存储物理相邻的前一个chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk ,也就是当前一个chunk是malloced,那么这个字段是无用的,为了不浪费,我们会把这块内存在必要时给到前一个chunk,比如我们在64位程序下,malloc(0x10),实际会分配0x20个字节,会包括chunk_head,free掉后,我们再次malloc(0x18),该chunk的高地址处的chunk的pre_size字段是无用的,会被利用起来,也就是会再次把之前malloc(0x10)的chunk给到我们,这就是 chunk 中的空间复用
size
chunk的大小包括chunk_head,是安照2 * SIZE_SZ(字长)对齐的,也就是32位是0x8,64位是0x10,恒是8的倍数,这样后三位bit位恒为0,我们将其设置为标志位
- NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。 ==A==
- IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。 free_chunk的始终为0 ==M==
- PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。==P==
fd bk
allocated状态下从这里开始为用户数据区,也就是我们通常说的malloc返回给用户的地址就是从这里开始的,free状态下会被添加到对应的空闲管理链表中
fd 指向下一个(非物理相邻)空闲的 chunk
bk 指向上一个(非物理相邻)空闲的 chunk
fd
和bk
是forward
和backward
的缩写。fd_nextsize bk_nextsize
free时使用,用于large chunk
fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。
一般情况下,物理相邻的两个空闲 chunk 会被合并为一个 chunk 。堆管理器会通过 prev_size 字段以及 size 字段合并两个物理相邻的空闲 chunk 块。
bin
用户释放掉的 chunk 不会马上归还给系统,会放入bin或者合并到top chunk中去,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk。当用户再一次请求分配内存时,ptmalloc 分配器会试图在空闲的 chunk 中挑选一块合适的给用户。这样可以避免频繁的系统调用,降低内存分配的开销。==mmap申请释放时直接归还操作系统 不进入bin==
ptmalloc2维护bins数组
根据空闲的 chunk 的大小以及使用状态将 chunk 初步分为 4 类:fast bins(小),small bins(中),large bins(大),unsorted bin(未整理)。
- 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的 chunk 比较杂。
- 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
- small bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。
fast bin
malloc state 中的 fastbinsY[]
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 |
- ptmalloc2 为了提高分配的速度,会把一些小的 chunk 先放到 fast bins 的容器内,fast bins P位始终为1,不合并物理相邻的chunk
- 是个单链表结构,只是用fd成员
- 用户申请chunk大小<=MAX_FAST_SIZE时,优先从fastbins查找空闲块,LIFO规则,满足频繁的申请和释放同一块chunk,满足局部性
- ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就是设置 fast bins 中 chunk 的最大值。当 MAX_FAST_SIZE 被设置为 0 时,系统就不会支持 fastbin 。
unsortedbin
- 可以视为空闲 chunk 回归其所属 bin 之前的缓冲区,刚刚被释放,还未分类的chunk。bin[1]
- FIFO的双向链表,当执行
free()
函数时,内存分配器会根据释放的内存块的大小,将它们放置到不同的内存块链表中。对于小于等于FASTBIN_MAX_SIZE
的内存块,它们会被放置到fastbin
中,而对于大于FASTBIN_MAX_SIZE
的内存块,则会被放置到unsorted bin
中。存放未被整理的 chunk - malloc时,内存分配器首先会尝试从
fastbin
中分配空闲内存块,当分配器需要分配大于FASTBIN_MAX_SIZE
的内存时,它会尝试从unsorted bin
中分配内存块。如果分配器发现unsorted bin
中有适合大小的空闲块,它将选择其中一个,并返回给应用程序。如果没有适合的空闲块,则分配器会将unsorted bin
中的空闲块合并,并将合并后的大块添加到large bin
中。会依次查找small bin
、large bin
、mmapped
等其他内存块,直到找到合适的内存块。
smallbin
size= 2* size_sz * index
下标 | SIZE_SZ=4(32 位) | SIZE_SZ=8(64 位) |
---|---|---|
2 | 16 | 32 |
3 | 24 | 48 |
4 | 32 | 64 |
5 | 40 | 80 |
x | 2*4*x | 2*8*x |
63 | 504 | 1008 |
- 每个链表中存储的 chunk 大小都一致
- 循环双向链表,每个链表都有链表头结点
- small bins 中每个 bin 对应的链表采用 FIFO 的规则
- 释放的时候会检查相邻的是不是free的,如果是进行合并然后放到 unsortedbin
large Bin
bins[64] ~ bins[126]
循环双向链表 FIFO
这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致,具体如下:
组 数量 公差 1 32 64B 2 16 512B 3 8 4096B 4 4 32768B 5 2 262144B 6 1 不限制
heap_info
一般申请的 heap 是不连续的,因此需要记录不同 heap 之间的链接结构。
该数据结构是专门为从 Memory Mapping Segment 处申请的内存准备的,即为非主线程(mmap申请)准备的。
malloc_state
该结构用于管理堆,记录每个 arena 当前申请的内存的具体状态,比如说是否有空闲 chunk,有什么大小的空闲 chunk 等等。无论是 thread arena 还是 main arena,它们都只有一个 malloc state 结构。会在最新申请的arena中
struct malloc_state { |