堆管理器:Ptmalloc2 (HEAP MANAGER)

HEAP MANGER

为什么要有堆管理器

堆是动态分配的虚拟内存(只有在程序访问到这块虚拟内存时,系统才会建立虚拟内存和物理内存的映射,称为延迟绑定机制,大大节省了内存),从低地址向高地址生长,程序可能需要频繁的向操作系统申请内存的系统调用,与此同时就伴随这从用户态到内核态的多次转换,转换的同时需要保存context,影响程序性能,堆管理器就出现了,在操作系统和用户当作中间人,他会申请较大的内存,管理分配,1、响应用户申请内存的请求2、管理用户所释放的内存,堆管理器并非由内核实现的,Linux标准发行版中使用的堆分配器是glibc中的堆分配器:ptmalloc2

平台:操作系统 --> 堆管理器 --> 用户
资源:物理内存 --> arena -->可用内存

image-20230817101448007

堆管理器ptmalloc2

arena 指的是管理器在第一次malloc时申请到的大内存快,堆内存区域本身,并不是结构,可以理解为内存池;主线程的 main arena通过==(s)brk==创建,没开alsr分布在数据区(.data/.bss)的结尾,开启后在尾部随机偏移处;其他线程的 arena包括一些主线程的大内存 是通过 ==mmap== 创建,分布在Memory-mapped segment,也就是我们glic动态链接库加载的区域

malloc_statemain 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后剩余的部分

image-20230225112707364
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
/*above chunk——head*/
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-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 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

    fdbkforwardbackward的缩写。

  • 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(未整理)。

  1. 第一个为 unsorted bin,字如其面,这里面的 chunk 没有进行排序,存储的 chunk 比较杂。
  2. 索引从 2 到 63 的 bin 称为 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
  3. 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
  1. ptmalloc2 为了提高分配的速度,会把一些小的 chunk 放到 fast bins 的容器内,fast bins P位始终为1,不合并物理相邻的chunk
  2. 是个单链表结构,只是用fd成员
  3. 用户申请chunk大小<=MAX_FAST_SIZE时,优先从fastbins查找空闲块,LIFO规则,满足频繁的申请和释放同一块chunk,满足局部性
  4. ptmalloc 默认情况下会调用 set_max_fast(s) 将全局变量 global_max_fast 设置为 DEFAULT_MXFAST,也就是设置 fast bins 中 chunk 的最大值。当 MAX_FAST_SIZE 被设置为 0 时,系统就不会支持 fastbin 。
  5. image-20230225144619435

unsortedbin

  1. 可以视为空闲 chunk 回归其所属 bin 之前的缓冲区,刚刚被释放,还未分类的chunk。bin[1]
  2. FIFO的双向链表,当执行free()函数时,内存分配器会根据释放的内存块的大小,将它们放置到不同的内存块链表中。对于小于等于FASTBIN_MAX_SIZE的内存块,它们会被放置到fastbin中,而对于大于FASTBIN_MAX_SIZE的内存块,则会被放置到unsorted bin中。存放未被整理的 chunk
  3. malloc时,内存分配器首先会尝试从fastbin中分配空闲内存块,当分配器需要分配大于FASTBIN_MAX_SIZE的内存时,它会尝试从unsorted bin中分配内存块。如果分配器发现unsorted bin中有适合大小的空闲块,它将选择其中一个,并返回给应用程序。如果没有适合的空闲块,则分配器会将unsorted bin中的空闲块合并,并将合并后的大块添加到large bin中。会依次查找small binlarge binmmapped等其他内存块,直到找到合适的内存块。
  4. image-20230225150843293

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
  1. 每个链表中存储的 chunk 大小都一致
  2. 循环双向链表,每个链表都有链表头结点
  3. small bins 中每个 bin 对应的链表采用 FIFO 的规则
  4. 释放的时候会检查相邻的是不是free的,如果是进行合并然后放到 unsortedbin
  5. image-20230225151846846

large Bin

  1. bins[64] ~ bins[126]

  2. 循环双向链表 FIFO

  3. 这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致,具体如下:

    数量 公差
    1 32 64B
    2 16 512B
    3 8 4096B
    4 4 32768B
    5 2 262144B
    6 1 不限制
  4. image-20230225152540343

heap_info

一般申请的 heap 是不连续的,因此需要记录不同 heap 之间的链接结构。

该数据结构是专门为从 Memory Mapping Segment 处申请的内存准备的,即为非主线程(mmap申请)准备的。

malloc_state

该结构用于管理堆,记录每个 arena 当前申请的内存的具体状态,比如说是否有空闲 chunk,有什么大小的空闲 chunk 等等。无论是 thread arena 还是 main arena,它们都只有一个 malloc state 结构。会在最新申请的arena中

struct malloc_state {
/* Serialize access. */
__libc_lock_define(, mutex);

/* Flags (formerly in max_fast). */
int flags;

/* Fastbins */
mfastbinptr fastbinsY[ NFASTBINS ];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[ NBINS * 2 - 2 ];

/* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
unsigned int binmap[ BINMAPSIZE ];

/* Linked list, points to the next arena */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

分配和释放过程

image-20230805140153900