堆是下图中绿色的部分,而它上面的橙色部分则是堆管理器
我们都知道栈的从高内存向低内存扩展的,而堆是相反的,它是由低内存向高内存扩展的
堆管理器的作用,充当一个中间人的作用。管理从操作系统中申请来的物理内存,如果有用户需要,就提供给他。
注意:linux使用glibc
这里有两种申请内存的系统调用:
第一种brk,是将heap下方的data段(bss属于data段),向上扩展申请的内存。
第二种mmap,其实下图中的shared libraries叫做mmap区域,也就是内存映射。如果使用这种方式申请内存,那么就在这块区域内开辟新的内存空间。
主线程可以用brk和mmap,如果主线程申请的空间过大,那么会使用mmap;如果申请的空间比较小,那么就会再data段上向上扩展一段空间
子线程只能使用mmap段
malloc就是向堆管理器申请一块内存空间
free就是将申请来的内存空间归还给堆管理器
用户使用malloc向堆管理器要内存,堆管理器通过brk和mmap向操作系统要内存
首先了解三个关键词:
堆管理器可以与用户的内存交易发生于arena中
可以理解为堆管理器向操作系统批发来的有冗余的内存库存
每一个线程中都有一个arena分配区,每一个分配区都有一个控制结构
chunk是内存分配的最小单位,也是我们malloc过来的内存
chunk的size控制字段的最后三位分别是A、M、P
A代表是否是主线程arena中分配的内存
M代表这段区域是否是MMAP的
P用于标识上一个chunk的状态。当它为1时,表示上一个chunk处于释放状态,否则表示上一个chunk处于使用状态
我们来了解malloc_chunk各个成员的功能
chunk有4种:
alloced_chunk
free_chunk
top chunk
ast_remainder chunk
chunk的结构体如上图,但是我们发现其实除了large bin free chunk之外,其他的chunk都没有用结构体中的所有变量
首先来看一个程序
我们申请了一个0x100空间大小的heap,用空指针prt指向malloc返回的地址,然后再通过free()函数释放这段空间
我们用gcc编译一下,得到了一个a.out的elf文件
我们使用gdb对这个elf文件进行调试
我们执行到malloc执行完毕的时候查看vmmap
我们可以看到两个细节:
第一个细节:虽然我们申请的是0x100大小的heap,但是这里第一次申请却有0x21000大小的区域。为什么会申请这么大的空间呢?这个就与我们刚刚了解到的arena有关了
我们知道操作系统会将内存分配给堆管理器,然后堆管理器再调用给用户。
这个过程我们可以怎么理解呢?
就像堆管理器向操作系统批发了一大块内存空间,然后再对用户进行一小份一小份的售卖。
所以我们这里看到的0x21000大小的区域其实是操作系统给堆管理器的(也就是我们上面说的top chunk),然后我们的第二次调用malloc就从这一大份的内存空间中给出
第二个细节:我们发现我们申请的heap区域是在data段的高地址处,这也印证了我们刚刚说的如果主线程申请的内存区域比较小,那么是通过brk的方式在data段的高地址申请一块区域
一个小插曲:
我们想知道在x64下,能最小分配的堆空间是多大呢?
我们继续在刚刚的gdb调试中,输入fastbin
我们最小的chunk被free掉之后就会放入fastbin中,可以看到最小的fastbin是0x20的大小,为什么是0x20的大小呢?
首先在x64下,一个地址的内存大小就为0x8,那么我们的一个最小的chunk,就像上图一样,用pre size记录上一个chunk大小,用size记录自己的大小,size下面是一个fd,在下面是data,所以如果要最小的话,一共是4个0x8,那么就是0x20的大小
那么同理,在x86下,一个地址的内存大小为0x4,所以就是上面的图从中砍了一半,剩下左半部分是有效的,那么最小的堆在x86中就是0x10的大小
回到主线:
我们在test.c中使用malloc申请的是0x100的大小空间,但是实际上,堆管理器会给我们0x110的chunk,这多出来的0x10实际上就是prev size和size的大小,我们能够使用的data段就是这个0x100大小空间
这个时候我们又有一个问题了,我们是通过空指针prt当再malloc的返回值,那么我们的ptr指针在哪里呢?其实我们pte指针是指向0x100这个数据段的,而并非prev size这个chunk的开头部分
我们再回到调试,输入heap观察堆,可以发现我们申请的0x100大小的空间其实是0x111,这是为什么呢?(其他的heap、chunk区域可能是程序的缓冲区之类的)
这个0x111其实是0x100+0x10+0x1得来的
0x10就是prev size+size的大小
0x1其实是size最后的3bit中的P=1
然后我们再来看ptr这个指针,我们刚刚说了ptr这个malloc返回的指针处在size之后的data段开头
我们申请的0x100大小的heap的addr是0x55555555559290,而我们ptr这个指针指向的地址是0x555555552a0,我们发现其实是heap的addr+0x10,也就是在pre size和size之后,印证了我们刚刚的结论
再来一个小插曲:
这个插曲是关于prev size的覆用
首先说一个结论,我们申请0xn0大小的空间和申请0xn8大小的空间,堆管理器给我们的内存是一样的,为什么呢?
因为prev size的作用是记录相邻的低地址的free chunk的大小,而如果prev size上面是一个malloced chunk,那么prev size就没有作用了,这个时候堆管理器体现出了节省内存的思想,将prev size进行覆写,从而获得0x8的内存大小
bin是什么?在英文中,bin是垃圾桶的意思,就如字面意思一样,bin是管理堆的回收。
bin管理arena中空闲的chunk的结构,并且以数组的形式存在,数组元素为相应大小的chunk链表的链表头。bin存在于arena的malloc_state中
在chunk被释放的时候,glibc会将它们重新组织起来,构成不同的bin链表。当用户再次申请的时候,就会从其中寻找合适的chunk返回给用户。
不同大小区间的chunk被划分到不同的bin中,再加上一种特殊的fast bin,一共是4种:fast bin、small bin、large bin、unsorted bin
关于chunk中的链表有两种:
在bin中我们一般都是讨论逻辑链表
fastbins如下图所示,我们可以从中看出逻辑链表的结构特点
逻辑链表的好处是什么呢?如果我们想要再free之后重新申请一块区域,这个时候在bins中就会寻找适配的bin来还原内存空间。而这些空间恰好是被逻辑链表连在一起的,这样就可以提供刚好合适的内存空间给用户,不会造成浪费
bin有两种结构:双向链表和单向链表,除了fastbin是单向链表,其余的bin都是双向链表
我们的bin中有两个bin数组:
在实践中,一个被释放的chunk常常很快就会被重新使用,所以将其先放入unsorted bin中,可以加快分配的速度。
small bin使用频率介于fast bin和large bin之间。刚刚也提到了在unsorted bin 遍历的时候,fast bin可以变为small bin。
bin[64]~bin[126]
63个循环双向链表
先进先出(FIFO)的工作特性
管理大于504 bytes的free chunks(32位下)
large bin被分为了6组,每组bin能够容纳的chunk按顺序排成了等差数列,如下图所示
large bin为了加快检索速度,fd_nextsize和bk_nextsize指针用于指向第一个与自己不同大小的chunk。所以只有在加入了大小不同的chunk时,这两个指针才会被修改。
这一块等到学到了再补上吧