写在前面
堆怎么这么麻烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦烦
相关地址:
源仓库: https://sourceware.org/git/?p=glibc.git;a=summary
代码阅读: https://codebrowser.dev/glibc/glibc/
堆的对外关系
在用户层面,可以通过malloc
方法开辟一块内存空间使用,他的实现实际上是向堆管理器去索要内存空间,并不与直接的操作系统和物理内存去交互。堆管理得知用户的请求之后会通过系统调向物理内存索要一大块位置,然后拿出用户想要的大小给用户使用,等下次用户想要继续开辟空间的时候,他通过第一次索要的一大块位置继续给用户分配,等一大块位置都不够了的时候再次通过系统调用去向物理内存索要一大块区域的形式来减少系统调用。
他们的关系可以理解为下面的结构
物理内存 <-> 堆管理器 <-> 用户
堆Chunk结构
malloc_chunk源码
参考地址: https://codebrowser.dev/glibc/glibc/malloc/malloc.c.html#malloc_chunk
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
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;
};
上面malloc_chunk
是一个chunk的完整结构,用户去开辟内存实际就是拿到了一个堆的chunk,这些字段的作用分别如下
字段 | 作用 |
---|---|
mchunk_prev_size | 该字段记录物理相邻的前一个chunk的大小(低地址chunk)。如果前一个chunk处于空闲,则该字段记录前一个chunk大小;如果前一个chunk已经被使用,则该字段空间可以被前一个chunk的用户数据空间复用 |
mchunk_size | 该字段是当前chunk的大小。该字段的低三个比特位对 chunk 的大小没有影响,所以被复用为标志位. |
fd | fd 指向下一个(非物理相邻)空闲的 chunk。 |
bk | bk 指向上一个(非物理相邻)空闲的 chunk。 |
fd_nextsize | 仅在 large bin 中使用,指向大小不同的下一个空闲 chunk |
bk_nextsize | 仅在 large bin 中使用,指向大小不同的上一个空闲 chunk |
其中最后面两个字段fd_nextsize和bk_nextsize只在largebin中才会去使用。
malloced chunk结构
用户可以通过malloc
来去开辟空间,他会返回一个chunk的data指针位置,具体的结构图如下
![[Pasted image 20250415132102.png]]
prev size 是上一个物理chunk的完整大小,size是整个chunk的大小,例如说在32位系统中,我们通过malloc(8)
开辟一块空间,那么data部分的大小就是8,prevsize和size各占4字节,那么size实际记录的大小就是16。后面的data为数据内容。
free chunk结构
当chunk创建之后可以通过free(chunk_ptr)
来释放创建的chunk,chunk_ptr即chunk的指针。释放之后结构会发生变化,如下图
![[Pasted image 20250415132706.png]]
释放之后,data段的前两位,如果是32位系统那就是两个4字节,64即两个8字节,变成fd、bk字段,fd会指向下一个被释放的chunk(free chunk),bk则是上一个被释放的chunk(free chunk),这两个字段的作用主要是在堆管理器的bins中,后面会讲。
chunk的控制位
上面说了size是整个chunk的大小,以32位为例,prev size占4字节,size4字节,fd4字节,bk4字节一公式16字节,他们都是必须要存在的字段,也就是size大小至少为16字节,并且因为结构的原因,他肯定是能被8整除的,也就是这个size肯定是8的倍数,如果是8的倍数在二进制中后三位肯定是0,8的二进制即1000
,这三个位置刚好可以当作控制位,分别是AMP。AMP的作用如下
标志位 | 全称 | 作用 |
---|---|---|
A | NON_MAIN_ARENA | 记录当前的chunk是否不属于主线程(main_arean)1表示不属于,0表示属于 |
M | IS_MAPPED | 记录当前chunk是否是由mmap分配的 |
P | PREV_INUSE | 如果紧贴的上一个Chunk被分配了,那么他就是1否则是0 |
chunk大小证实
我们通过下面C代码来创建一个chunk,并通过gdb去查看以下在内存中chunk的大小,源代码如下
#include <stdio.h>
#include <stdlib.h>
int main(){
void* ptr = malloc(0x100);
free(ptr);
}
通过命令gcc -g main.c
来编译,-g
可以在gdb调试的时候显示源码,下面看一下在运行完创建chunk之后的chunk状态
![[Pasted image 20250415151300.png]]
size的值是0x110,加上标志位是0x111,其中0x100是我们开辟的data,然后prev size
和size
是各占8字节,16进制就是0x10即0x110,然后最后的0x1是控制位置,这里的1就代表P目前是1,即紧贴的上一个chunk是被分配的。
chunk的结构图
![[Pasted image 20250415130431.png]]
堆全局状态
malloc_state源码
参考地址: https://codebrowser.dev/glibc/glibc/malloc/malloc.c.html#malloc_state
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* 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. 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;
};
上面malloc_state
是一个堆全局状态的一个结构,他们的作用如下
字段 | 作用 |
---|---|
\_\_libc_lock_define | 同步访问互斥锁 |
flags | 用于标记当前主分配区的状态 |
have_fastchunks | 标记是否存在空闲的 fastbin 块(布尔值,但用 int 保证原子性),避免无 fastbin 时仍需遍历 |
fastbinsY | 存放 fastbin 链表的数组(单链表,LIFO) |
top | 指向 top chunk 的指针(未放入任何 bin 的剩余内存) |
last_remainder | 最近一次分割 small bin 后剩余的 chunk(优化小内存分配的局部性) |
bins | 常规 bins 的链表数组(包含 unsorted/small/large bins) |
binmap | 位图标记哪些 bin 非空(加速分配时的搜索) |
next | 连接所有分配区的全局链表(主分配区在头部) |
next_free | 空闲分配区链表(未被任何线程绑定的分配区) |
attached_threads | 绑定到此分配区的线程数(0 表示该分配区在空闲链表) |
system_mem | 当前分配区从系统分配的内存总量(通过 brk/mmap) |
max_system_mem | 当前分配区从系统分配的内存峰值(用于统计和限制) |
bins垃圾箱
如果一个chunk被free掉那么他们就会被划分到对应的bins中,bins分为多种,fast bin
、unsorted bin
、small bin
、large bin
、tcache bin
,然后fastbin是单独放在fastbinsY
字段的,其他的bin都存放在bins
中,后面会分别介绍他们。
Fast Bin
Fast Bin是一个单向链表的结构,一些比较小的空间被堆管理器释放的时候都会放到这里,他的数量默认为7个(可以通过某些手段修改),每一个都负责不同的大小的chunk表链,在32位下他们默认七个负责的chunk从0索引开始依次是16(0x10)
、24(0x18)
、32(0x20)
、40(0x28)
、48(0x30)
、56(0x38)
、64(0x40)
,如果是64位的话就基于32位的大小*2。然后就是chunk的p位总保持为1。他的进出规则是,谁在链表最后谁先出去,类似于栈的进出规则。他的结构如下
![[Pasted image 20250415193504.png]]
Unsorted Bin
Unsorted BIn是一个双向链表的结构,他在bins中的[1]的位置,用来管理刚刚释放但是还没分类的到BIN中的Chunk,系统每次释放chunk的时候不会立马去分类,而是都放到Unsorted Bin中,可以理解为归属于其他bin之前的缓冲区。当再次申请内存的时候,回到unsorted bin中寻找符合的chunk,如果没有则触发free的合并,然后重新构建表链去寻找符合的。他的进出规则是先进的先出。他的结构图如下
![[Pasted image 20250415194241.png]]
Small Bin
Small Bin与Unsorted BIn的结构基本差不多,主要区别为上层多了一层bin,进来的free chunk的大小都是相同类型的。他在bins[2]~bins[63]的位置中,进出规则是先进先出,在32位下他负责16
、24
、32
、40
......、504
大小的chunk,然后每个链表中的存储大小都一致。(64位的话就*2),他的结构如下图
![[Pasted image 20250415194937.png]]
Large Bin
Large Bin是用来存放巨大的free chunk,他有63个循环双向链表,进出规则是先进先出,在32位中负责管理大于504大小的free chunk。结构和SmallBin基本一样,但是他的链表大小是可以不同的,相比与其他的free chunk他多出了两个字段结构
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
用来指向对应freechunk大小的字段。