此篇文章是我参与 #青训营笔记创作活动 的第8篇笔记
主要是简单记录一下自己在深入学习堆时遇到的一些小要点,
只是个人的一个学习记录,水平不高还望大佬们包容。
堆的宏观结构
复习一下, 从总体上看可以把堆的数据结构分为微观结构和宏观结构:
-
微观结构主要是malloc_chunk的数据与结构:
- prev_size
- size
- fd,bk
- fd_nextsize, bk_nextsize
-
宏观结构包括堆块的宏观信息,可以使我们更好地了解glibc的堆管理实现,宏观结构分为以下几个部分:
-
arena & main_arena:可以看作是堆块的管理器,但其实就是堆内存本身
-
bin:用链表结构管理被free的malloc_chunk(堆块)
-
top_chunk:处于一个arena的最顶部(即最高内存地址处)、不属于任何bin的一片内存
-
malloc_state:管理 arena 的核心结构,包含堆的状态信息、bins 链表等
- main arena 对应的 malloc state 结构存储在 glibc 全局变量中
- 其他线程 arena 对应的 malloc_state 存储在 arena 本身中
-
arena & main_arena
-
区分二者
- arena 指的是堆内存区域本身,并不是结构;
- 主线程的 main arena 通过 sbrk 创建,管理所有堆块的结构体
- 其他线程的 arena 通过 mmap 创建,有时可能或会被
不恰当地称为arena,存在于线程的控制块plt中 -
不是每个线程都会有对应的arena 因为每个系统的核数有限,当线程数大于核数的二倍时,就必然有线程处于等待状态,所以没有必要为每个线程分配一个arena 32bit --> arena_num = 2 * core 64bit --> arena_num = 8 * core
-
主线程的 main arena和其他线程的 arena 可以看作是主分配区和非主分配区
- ps:ptmalloc为什么要增加非主分配区?(问题与回答复制自:malloc内存管理总结_)
- 答:如果没有非主分配区,所有的线程在主分配区上操作,互相竞争锁的过程十分影响分配效率。ptmalloc中增加了非主分配区支持,主分配区和非主分配区用环形链表进行管理,提高malloc的分配效率。 申请小块内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作。每个加锁操作大概需要5~10个cpu指令,而且程序线程很多的情况下,锁等待的时间就会延长,导致malloc性能下降。一次加锁操作需要消耗100ns左右,正是锁的缘故,导致ptmalloc在多线程竞争情况下性能远远落后于tcmalloc。最新版的ptmalloc对锁进行了优化,加入了PER_THREAD和ATOMIC_FASTBINS优化,但默认编译不会启用该优化,这两个对锁的优化应该能够提升多线程内存的分配的效率。
-
如何标志
- 在malloc_chunk 中的 size 的倒数第三个标志位 A,多线程时为1,主线程为0
- NON_MAIN_ARENA,A:size的倒数第三位表示当前chunk属于主分配区(0)还是非主分配区(1)
-
子线程的堆和主线程的堆是不一样的
- 每个线程都会预分配一个堆空间
- 线程会从这个对空间创建top_chunk和堆块
- 当malloc的空间超过预分配的大小,会回到main_arena之前再次分配一个空间
- 如果线程的堆存在溢出,可以用之前的chunk越界写堆的arena结构
一些 tips
- 定位子线程的chunk的技巧
- 向子线程的堆块输入特殊值:"0xdeadbeef"
- 在gdb使用 search -4 0xdeadbeef
- 搜索出来的地址即堆的地址
- 多线程利用思路
- 在子线程中找到堆空间的地址空间A
- 在A中找到恢复线程的arena的结构
- 通过arena的结构尝试堆利用
malloc_state
管理 arena 的核心结构,包含堆的状态信息、bins 链表等
- main arena 对应的 malloc state 结构存储在 glibc 全局变量中
- 其他线程 arena 对应的 malloc_state 存储在 arena 本身中
Arena 头部结构:malloc_state 存储了 arena 的状态,其中的 bins[] 用于管理空闲块的 bins
struct malloc_state
{
/* Serialize access. */
mutex_t 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 */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. */
struct malloc_state *next_free;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
主要关心这么几个:
- 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链(64126)
bin
bin负责管理free的malloc_chunk,按照free的chunk大小划分为以下几种:
-
fast bins,用于管理较小的 chunk
-
small bins,用于管理中等大小的 chunk
-
large bins,用于管理较大的 chunk
-
unsorted bin,用于存放未整理的 chunk
-
tcache
- 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术(see commit),目的是提升堆管理的性能。但提升性能的同时舍弃了很多安全检查,也因此有了很多新的利用方式。
- 管理流程
- malloc/free --> glibc --> arena --> fastbin/bins -->smallbin/largebin/unsortedbin
- 从glibc找到main_arena
- 在main_arena的管理结构体malloc_state通过固定偏移中找到fastbinsY[NFASTBINS],用以管理fastbin。
- 找到bins[NBINS * 2 - 2],用以管理unsortedbin。
- bin的放置顺序
索引为1的是unsortedbin,这里面的chunk没有进行排序,比较杂乱。 索引从2到63的bin称为small bin,同一个small bin链表中的chunk的大小相同。两个相邻索引的small bin链表中的chunk大小为2个机器字节,即32-->4字节,64-->8字节。 索引从64到126的bin被称为large bin。large bins中的每一个bin都包含一定范围内的chunk,其中的chunk按fd指针的顺序从大到小排列,最靠近bin头的越大,相同大小的chunk按照最近使用顺序排列。
- 任意两个物理相邻的空闲chunk不能在一起,否则会合并。
- free之后的chunk,与top_chunk相邻的,会与top_chunk合并,不与之相邻的,会根据其大小进入到不同的bin
小的进入fastbin,大的进入unsortedbin 此时,释放掉的chunk不会马上归还系统,ptmalloc会统一管理heap和mmap映射区域的空闲的chunk。 当用户再一次请求分配内存时,ptmalloc分配器会试图在空闲的chunk中挑选一块合适的给用户,这样可以避免频繁的系统调用,减少内存分配的开销。
- 需要注意的是,并不是所有的chunk被释放之后立即放到bin中。ptmalloc为了提高分配的速度,会把一些小的堆块先放到fast bin的容器内。而且fast bin容器中的chunk的使用标记总是被置为1的,所以不会自动合并。
fastbin
单向链表后进先出,同时 p 位被保留(设置值为1)防止合并,同一大小的 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 |
- 注意fastbin不属于bins,不是bins管理的
- fashbin是ptmalloc单独用来管理0x20-0x80(64位平台)的堆块的数据结构,如果free的chunk大小在0x20-0x80之间,会优先进入fashbin,
smallbin
- 大小在0x20-0x400(64位)
| 下标 | x86(size_t=4) | x64(size_t=8) |
|---|---|---|
| 2 | 0x10 | 0x20 |
| 3 | 0x18 | 0x30 |
| 4 | 0x20 | 0x40 |
| 5 | 0x28(40) | 0x50(80) |
| x | 2 * 4 * x | 2 * 8 * x |
| 63 | 0x1F8(504) | 0x3F0(1008) |
- 双向链表,先进先出
- 释放的时候会检查相邻的是不是 free 的,如果是进行合并然后放到 unsortedbin
unsortedbin
堆块中转站
-
双向循环链表,先进先出
-
存放所有不满足 fastbin,未被整理的 chunk
- 除了fastbin管理的小堆块,free掉的chunk都是先进入到unsortedbin里再进行整理
-
malloc 的时候在其他 bin 没找到合适的就会遍历 unsortedbin 同时根据大小放到对应的 bin 里
-
在整理过程中,先将所有放在unsortedbin链上的堆块按照大小整理到其它链上
-
将fastbin上的碎片整理到unsorted,再由unsorted整理到其他bin链
largebin
- 管理大于0x400的堆块(64位),大于0x200的堆块(32位)
- 双向链表,先进先出
- 需要根据 fd_nextsize 和 bk_nextsize 指针从大到小排序
附录
学习与参考链接: