堆入门概述
堆入门概述
1 | 参考资料; |
什么是堆?
堆和栈不同,堆是(由操作系统内核或者堆管理器)动态分配的,是程序虚拟内存中由低地址到高地址增长的线性区域。一般只有当用户向操作系统申请内存时,这片区域才会被内核分配出来,并且出于效率和页对齐的考虑。通常会分配相当大的连续内存。程序再次申请时便会从这片内存中分配,直到堆空间不能满足时才会再次增长。
先看看堆在虚拟内存中的位置
堆的属性是可读可写的,大小通过brk()或sbrk()函数进行控制。如上图所示,在堆未初始化时,Program break指向BSS段的末尾,通过调用brk()和sbrk()来移动program_break使得堆增长。在堆初始化时,如果开启了ASLR,则堆的起始地址start_brk会在BSS段之后的随机位移处,反之则start_brk紧挨着BSS段。
两个函数定义如下:
1 |
|
堆的生长方向是低地址到高地址生长的,而栈是从高地址到低地址生长的。
实际上堆可以申请的内存空间要比栈大很多,在linux的4G的虚拟内存空间里最高可以达到2.9G的空间
mmap()和unmmap()
当用户申请的内存过大时,ptmalloc2会选择通过mmap()函数创建匿名映射段(不与任何文件关联)供用户使用,并通过unmmap()函数回收。
堆块的基本结构
pre size(prev size)和size头统称为chunk头(chunk header)
每次malloc申请得到的内存指针,其实指向user data的起始处。
堆的大致图解如下:
prev size和 size字段分别代表前一个chunk的大小和当前chunk的大小,在64位环境下两个分别都是8字节,共16字节,称之为chunk的头部字段。后面的user data是用户可以输入数据的地方。
为了节省空间,将size的最低三位设置为标志位,从高到低依此是:
- non-main-arena 用来记录当前chunk是否不属于主线程,1代表不属于,0代表属于。
- is-mmap表示当前chunk是否由mmap分配的,1表示属于,0表示不属于。
- prev-inuse用来表示前面紧邻的那个chunk是否正在使用,0表示前面的chunk已经被释放,1表示正在被用户使用
空间复用
prev size记录前面一个chunk的大小。注意,prev size只有在前面的chunk被free掉的时候才生效,也就是说,只有在prev_inuse为0的时候,系统才把prev_size字段当作prev size(记录前一个chunk的大小)
如果chunk正在被使用,那么他会把下一个chunk的prevsize字段当作userdata,充分利用空间。
也就是说,如果我们申请一个0x80长度大小的区域,系统实际给我我们0x90(0x10头部),如果我们申请0x88大小的区域,系统同样也会给我们0x90大小的区域(算上头部),剩下的8字节,使用nextchunk的prevsize才真正代表前一个chunk的大小。
特殊的堆块
- 最开始时,程序的堆还未被利用,整个的堆区域属于一个很大的堆块叫做topchunk。
- 当已经使用的空间不够时,程序就会从topchunk中分割一块出来给程序使用
重要概念和结构体
arena
这是Linux堆管理的常用术语。arena包含一片或数片连续的内存,堆块将会从这片区域划分给用户。
主线程的arena被称为main_arena,它包含start_brk和brk之间这片连续的区域。其实就是ptmalloc2堆管理器通过与操作系统内核进行交互申请到的。
因为是主线程分配的,所以叫做main arena,是通过brk()调用来扩展。
其它子线程的arena可以有数片连续内存。但是子线程分配的映射段大小是固定的,不可以扩展,如果映射段不够用的话需要再次调用mmap()来分配新的内存。
heap_info
子线程的arena可以有多片连续内存,这些内存被称为heap。每一个heap都有自己的head_info描述头。其定义如下,heap_info是通过(prev指针)链表相连接的,并且heap header里面面保存了指向其所属的arena的指针ar_ptr。
1 | // malloc/arena.c |
arena在ptmalloc2中对应的数据结构是malloc_state,定义如下:
malloc_state
1 | // malloc/malloc.c |
- 每个线程只有一个arena header,里面保存了bins、top chunk等信息。主线程的main_arena保存在libc.so的数据段里,其他线程的arena则保存在给该arena分配的heap里面。
堆块的管理
chunk 结构体:
1 | struct malloc_chunk { |
因为chunk被free之后,按常理来说用户不应该能够访问到这个chunk,所以在userdata区域存放一些用于管理内存的指针信息。
- fastbin:单链表结构,只有fd,是个单链表结构
- small&unsortedbin:双向链表结构,fd和bk都有
- largebin:双向链表,fd和bk都有,同时还有fd nexesize和bk nextsize
在ptmalloc2内存管理机制下,为了保证程序的快速运行,而且方便内存管理,所以ptmalloc2将释放后的堆块根据其大小分成不同的bin。不同大小区间的chunk被划分到不同的bin中,再加上一种特殊的bin,一共有四种
- fastbin:大小范围0x20~0x80
- smallbin:大小范围0x90-0x400
- Large bin:大小范围0x410以上
- unsortedbin:未被归类的bin,临时存储用,存储的堆块大小不一定多大。
这些bin记录在malloc_state结构中
- fastbinsY:这是一个bin数组,里面有NFASTBINS个fast bin。
- bins: 也是一个bin数组,一共有126个bin,按顺序分别是:
- bin 1为unsorted bin
- bin2到bin63为small bin
- bin64到bin126为large bin
fastbin
实践中,程序申请和释放的堆块往往都比较小,所以glibc对这类bin使用单链表结构,并采用LIFO(后进先出)的分配策略。并且在ptmalloc2下,为了加快速度,fast bin里的chunk不会进行合并操作,并且下个chunk的prv_inuse始终标记为1,使其始终处于使用状态。
unsorted bin
一定大小的chunk被释放时,在进入small bin或者large bin之前,会先加入unsorted bin。因为可以加快分配效率。并且采用FIFO(先进先出)的分配策略。
small bin
(以64位系统为例)
满足空间比fast bin大一点。如果程序请求的内存范围不在fast bin范围内,就会考虑small bin。简单点说就是大于80 bytes小于某一个值时就会选择它。
描述:
- 在32位系统中,当用户释放的堆块大小大于64B,小于等于512B时使用small bin进行管理
- small bin为双向循环链表,且使用FIFO(先入先出)
- 当满足small bin条件的chunk被释放后,会优先被放入unsorted bin,才会被分配到small bin中。
- 相邻的free chunk将会被合并成一个更大的free chunk,增加内存利用率。
- 由于small bin和fast bin有重合部分,所以这些chunk在某些情况下会被加入到small bin中。
largebin
large bin在bins里居第64位到第126位,共63个。
调用malloc时,程序在干什么
计算真正的堆块大小(加上头部长度,对齐)
在fastbin范围内?
- 是,检查对应的bin链表中有没有chunk
- 有,分配给用户,完成
- 是,检查对应的bin链表中有没有chunk
如果不在fastbin范围内,或者没有chunk可用
在smallbin范围内?
- 是,检查对应大小的bin链表中有没有chunk
- 有,取出来给程序,完成
- 是,检查对应大小的bin链表中有没有chunk
如果不在smallbin范围内,或者smallbin里面也没有。
unsortedbin中有没有chunk?
- 有的话,从尾部取出第一个chunk,看看大小是否满足需求。
- 满足,切分后大小是或否大于minsize?
- 大于,切分块,返回给用户,剩下的块放进unsorted bin
- 小于等于minsize,直接返回给用户,完成
- 满足,切分后大小是或否大于minsize?
- 不满足,把这个块放入small/largebin对应的链表中,继续遍历下一个块
- 有的话,从尾部取出第一个chunk,看看大小是否满足需求。
如果unsortedbin中所有的块也不能满足需求。
大小是否在largebin范围?
- 是,检查对应的bin链表中有没有符合的chunk。
- 有,找到满足需求最小的chunk,切分块返回,剩下的放进unsorted bin中。
- 是,检查对应的bin链表中有没有符合的chunk。
largebin也不行?再次遍历small/large找best fit的chunk。
还是没有,那就从topchunk中切割。
topchunk也不够?mmap系统调用。
当调用free时,程序干了什么
- free的chunk大小属于fastbin吗?
- 是,放进fastbin,完成。
- 是mmap分配的吗?
- 是,调用munmap
- 前一个chunk空闲吗?
- 是,向前合并。
- 后一个chunk是topchunk吗?
- 是,和topchunk合并,完成
- 后一个chunk是free的吗?
- 是,向后合并。
- 放进unsortedbin,完成。