什么是堆
在程序运行过程中,堆可以提供动态分配的内存,允许程序申请未知的内存。堆其实就是程序虚拟地址空间的一块连续线性区域,它由低地址往高地址增长。我们称管理堆的那部分程序为:堆管理器
堆管理器位于程序与内核之间,主要做:
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 内存块结构
在内存分配与使用中只有当真正去访问一个地址的时候,系统才会建立虚拟页面与物理内存的映射关系
Arena 头部结构:malloc_state 存储了 arena 的状态,其中的 bins[] 用于管理空闲块的 bins
1 | struct malloc_state |
主要关心这么几个:
mfastbinptr fastbinsY[NFASTBINS],保存了 fastbins 各个链表的数组的头,大小为 10 记录的是 fast bin 链
mchunkptr top,指向了 top chunk
mchunkptr bins[NBINS * 2 - 2],大小为 129。记录的是 unsorted bin(1)、small bin(263)、large bin 链(64126)
堆的基本操作
malloc
malloc(size_t n) size_t 是无符号数
返回对应大小字节的内存块指针,当 n 等于 0 的时候返回的是当前系统允许的堆的最小内存块,当 n 为负数是,程序会去申请很大的内存空间,通常会失败
free
free(void* p)
这个 p 指向的是想要释放的内存块,当 p 为空时,函数不执行任何操作
当 p 被释放后再次释放就会造成 double free
除了被禁用 mallopt 的情况下,当释放了很大的空间时,程序会将这些内存空间还给系统,以便于减少程序所使用的内存空间
内存分配背后的系统调用
前面的无论是 malloc 还是 free,这些函数背后的系统调用主要是 (s)brk 以及 mmap、munmap 函数
(s)brk
对于堆的操作,系统提供 brk 函数,glibc 提供了 sbrk 函数,我们可以通过增加 brk 的大小来向操作系统申请内存
初始时,堆的起始地址 start_brk 以及堆当前末尾 brk 指向同一地址,根据是否开启 ASLR,两者具体位置会有所不同
不开启 ASLR 的时候,start_brk 以及 brk 会指向 data/bss 段的结尾
开启 ASLR 的时候,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处
chunk 的结构
我们称由 malloc 申请的内存为 chunk,这块内存在 ptmalloc 中被称为 malloc_chunk 结构体表示
无论一个 chunk 的大小如何,处于分配状态还是释放状态,它们都使用一个统一的结构。虽然它们使用了同一个数据结构,但是根据是否被释放,它们的表现形式会有所不同
1 | /* |
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 指针才是真正返回给用户的内存指针。
空闲中的 chunk 结构
当 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 块
bin
ptmalloc 采用分箱式方法对空闲的 chunk 进行管理。首先,它会根据空闲的 chunk 的大小以及使用状态将 chunk 初步分为 4 类:
fast bins,用于管理较小的 chunk,在之前说的那个 fastbinY 数组里面
small bins,用于管理中等大小的 chunk
large bins,用于管理较大的 chunk
unsorted bin,用于存放未整理的 chunk
bin 对应的数据结构
1 |
|
看不下去