堆基础----2 开始入微数据结构

会有一股神秘感。 提交于 2019-11-29 23:48:22

有三个极其重要的堆数据结构是用于堆的管理

  • heap_info(Heap Header的存在) 线程即thread arena可能有多个堆,虽然起初每个arena只有一个堆段,但是随着malloc等函数消耗完堆空间后,新的堆会通过mmap到这个arena中,即受这个arena的管辖!!!,此时这就会有个新的堆段,一个堆段有一个heap_info的数据结构!!! main arena只有一个heap_info 即只有一个堆结构 所以不需要heap_info这个数据结构 所以只有thread arena需要heap_info结构来管理一到多个堆!!!
  • malloc_state(Arena Header的存在) 一个Arena可能会有多个堆 但是这些堆共有一个malloc_state的数据结构,malloc_state数据结构记录着bins, top chunk, last remainder chunk的信息!!!
  • malloc_chunk(Chunk Header的存在),一个堆由又可以划分为许多称为chunk的数据结构,每个chunk都有自己的Header头部信息

一个典型的main_arena和thread_arena的示意图如下

  • 对于main_arena而言,Top Chunk位于heap的最高地址处 其内部每一个Chunk都有一个malloc_chunk的头部结构(不论是在使用中的或者被free掉的) 同时对于main_arena而言,有一个malloc_state的结构是main_arena的头部信息!!!
  • 对于thread_arena而言,由于是mmap过来的,所以需要heap_info结构来将不同的heap串连在一起,而对于main_arena而言,它只需要调用brk或者sbrk系统调用即可完成空间的申请工作,而thread_arena则需要一个heap一个heap_info的结构来支持,当然 每一个线程只有一个malloc_state的结构来管理这个arena。也管理着这个thread_arena所有的heap结构,同时每个heap有多个chunk存在,其顶部仍然是Top Chunk,并且每个chunk都有一个malloc_chunk的头部结构
    在这里插入图片描述

对于一个线程的Arena,可能存在多个Heap段 示意图如下,可以看出一个Arena通过heap_info数据结构中相关的指针作为链表串接在一起 当然对于其他派生的heap,则没有malloc_state的结构,其顶部仍然是Top Chunk的数据结构
在这里插入图片描述

Chunk

下面开始就要仔细分析Chunk的类型了
Chunk的类型有以下四种

  • Allocated Chunk 即正在使用未被Free的Chunk
  • Free Chunk 之前被分配然后被释放掉的Chunk 当然这里所说的释放并不是将空间立即交还给操作系统 而是free之后先由ptmalloc管理 以减少系统调用带来的额外开销
  • Top Chunk 我们在上面两个图可以看到Top Chunk始终是位于一个Heap最顶端的地方,即最高地址处
  • Last Remainder Chunk

chunk的数据结构表达如下

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      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;
};

Allocated Chunk

处于使用状态的Chunk结构如下 即Allocated Chunk的数据结构如下 由以下几种成分构成

  • Prev_size 如果上一个chunk处于free状态 那么该域表示上一个chunk的大小
  • chunk_size 紧邻在Prev_size之后的字段 代表着该使用中的chunk的大小 由于与8对齐 所以低3bit位就被空闲下来
    • N位 代表是否是NON_MAIN_ARENA 为1代表该chunk属于一个thread_arena
    • M位 代表是否该chunk是否是通过mmap而来的 为1代表是mmap获得的chunk
    • P位 代表是否是PREV_INUSE 为1表示上一个块正在被使用 即处于Allocated状态
  • 值得一提的是 Allocated Chunk的下一个chunk的malloc_chunk数据结构的Prev_size字段是作为user_data的一部分来使用 因为Prev_size是针对于Free Chunk设置的 即记录的是Free Chunk的大小
    在这里插入图片描述

Free Chunk

Free Chunk的结构图如下所示 成分如下

  • 一个Prev_size字段 由于free chunk是不会毗邻的 当两个相近的chunk 都处于free状态 它会被组合成一个大的空闲块 所以这个块的malloc_chunk结构中的Prev_size字段是用作上一个块的user_data区域的(就是对free块没任何卵用)
  • size存储着这个free块的大小 N位代表该块是否是non_main_arena M代表是否是mapped P位代表着是否是Prev_inuse 由于不存在响铃的free 块 所以这个标志总是设置为1
  • fd指针 索引下一个块的一个指针 这个索引并不是指地址高低的下一个 而是指在同一个bin中的下一个逻辑块
  • bk指针 同理 索引上一个在相同bin的chunk块
    在这里插入图片描述

Bins概况(数组链表的数据结构 一个Bin即一个双向链表)

Bins是用来串连free块的一个list链表 用来管理free chunk的链表 基于chunk_size 有不同的bins 主要有以下四种类型的bins

  • Fast Bin
  • Unsorted Bin
  • Small Bin
  • Large Bin

Fast Bins

用来存储这些bins的数据结构有以下两种

  • fastbinsY 这是一个数组array + 链表 用来存放fast bins的
    • 被收入Fast Bin要求的chunk 尺寸是16-80bytes 在这个尺寸范围内的被称为Fast Chunk。在所有的bins中 fast bins的分配是非常快速的 所以被称为 fast bins
    • 每个fast bin包含一个单链表 之所以采用单链表是因为在fastbins中不会从中间摘下一个chunk出来,不论将chunk添加到fastbins还是从fastbins中删除一个chunk,都是只发生在单链表首尾之间的!!!
    • malloc初始化期间 ,fastbins中容纳的最大chunk size是到64并非80。所以此时收录的chunk_size在16到80bytes之间
    • 另外一点最重要的是fast chunk之间是不会相互合并的!!!,这样容易导致内存碎片 但是加速了chunk的释放
    • 当用户调用malloc的时候,最开始的时候由于fastbins还没有被初始化完毕 因此当用户索取一个fast chunk的时候, 是small bins的代码响应malloc的请求!!! 之后当fastbins不为空 会根据用户申请的user space转换为chunk_size然后获取相应的fastbins的index 找到对应的fast bin单链表 然后将第一个chunk块取下返还给用户使用!!!
    • 当用户调用free的时候, 若释放的chunk size在fastbins允许的范围内 那么会根据chunk_size计算出对应的fastbin的index 然后会被放到对应fastbin链表的第一个位置上

fastbins的示意图如下
在这里插入图片描述

Bins 包括了Unsorted Bin,Small Bin 以及 Large Bin

  • bins 这也是一个数组 + 链表,这个数组存放unsorted,small 和 large bins 总共有126个bins 其划分如下
    • Bin1 用来存放Unsorted Bin 只有一个Bin
    • Bin2-Bin63 用来存放Small Bin 有62个Bin
    • Bin64-Bin126 存放Large Bin 有63个Bin

Unsorted Bin

当small chunk或者large chunk被释放后 , 并不会将他们直接放入独自该去的bin中 而是将他们添加到Unsorted Bin上。 之所以这样是因为glibc可以最大限度的使用最近使用过的chunk。这将相当于Unsorted Bin是一个针对于Small Bins 和 Large Bins的一个高速缓冲 Unsorted Bin链表是一个双向的循环链表

Bins的结构图如下所示
在这里插入图片描述

Small Bin

  • Small Bins中收纳chunk_size小于512Bytes 注意不是小于等于 而是小于 512Bytes的chunk 这些chunk被称为small chunk
  • 每个small bin包含一个双向循环链表结构用以将small chunk进行串连 使用双向链表是因为有可能会从链表的中间取出一块chunk出来返还给用户!!!其增添和删除bin中chunk的操作是遵循FIFO的模式 即添加在Bin的开头 删除是在Bin的末尾
  • Small Bin的Bin与Bin之间是以8Bytes进行间隔的 例如Bin-2是收纳chunk_size为16Bytes的Small Bin。 Bin-3收纳chunk_size为16+8 == 24的Bin
  • Small Bins中的每一个Bin中存放的都是相同尺寸的chunk 所以不存在sort一说
  • 两个相邻的free small chunk是不存在的 它会被组合成一个大的free chunk 组合虽然减少堆碎片的产生 但是花费了额外的时间
  • 当malloc时申请了small chunk大小的chunk时,若small Bins中的所有Bin都是Null 这时候由unsorted bin的代码来负责处理这个malloc 并且在第一次调用malloc期间 small bin和large bin的fd和bk指针指向自身所在的bin 即指针得有个去向!当随着程序的运行和malloc以及free的交替 此时若用户再次调用malloc申请chunk size在small bin范围内大小空间时 此时会从对应index的Small Bin中摘取末尾的chunk块返还给用户使用
  • 当用户调用free释放掉 small chunk的时候 我们知道Small chunk不允许其前后也是空闲块 所以就需要一个额外的check操作 即检索prev的chunk和next_chunk是否是空闲状态 若因此检索是的话,那么就需要对前或者后的chunk块进行unlink操作 即从对应的Small Bin上进行摘除操作。然后整合成一个大的chunk块 送入到Unsorted Bin中

Large Bin

Large Bin 中收纳的是大于等于512Bytes的chunk块。Large Bin在Bins中的范围是64-126 64正好是26 这样设计也是为了方便查找index设计的吧。Large Bins中的每个Bin也是一个双向循环链表!!!LargeBins中的Bin数量是63个

  • 在这63个Bins中 其之间的间隔Bytes并不是固定的 而是遵循以下规律
    • 从64开始的32个bins是以64Bytes为间隔的 并且不像Small Bins中一样 每个bin中并不是存储大小一定的large chunk 而是存储在一定范围内的chunk例如Bin-65是存储了512-568Bytes这个区间内的large chunk
    • 16bins他们的间隔是512Bytes
    • 8个bins他们的间隔是4096Bytes
    • 4个Bins间隔是32768Bytes
    • 2个Bins的间隔是262144 bytes apart
    • 一个Bin包含一个块的Remining Size
  • 正是因为每个Large Bin中存放的chunk size是一个浮动的值 所以需要进行降序排序,即大chunk块放入链表头 之后的chunk_size 依次递减
  • 两个free的块也是无法相邻的,所以会被整合后统一先放入到Unsorted Bin中
  • 当用户调用malloc申请large chunk的时候 ,第一次由于所有的Large Bins都为null 所以此时服务该malloc的代码是** next largest bin code** 在第一次调用malloc的时候 此时small bins和large bins会被初始化 即fd和bk指针指向本身所属的Bin结点。当之后Large Bins不为空之后 首先判别在对应index的Large Bin中是否存在着大于用户申请的chunk块 若存在最大尺寸的块大于用户申请的尺寸 则从末段 即最小尺寸的chunk开始向后遍历 找到第一个大于等于用户申请尺寸的chunk 然后这个块肯定不会直接返还给用户 因为直接给有可能造成浪费情况 所以这个chunk会被split成两个块一个自然是用户所需要的User Chunk 这个Chunk返还给用户使用 剩余的chunk 即Remainder Chunk会被添加到 unsorted bin中去。如果根据用户的需求找到对应的Large Bin链表但是发现最大的chunk块尺寸小于用户所需要的尺寸 那么这个时候就需要找到下一个不为空的Large Bin。这个时候就需要检索binmap,如果能找到符号条件的Bin 则仍然按照split两块的形式 一块返还给用户 一块挂入到Unsorted Bin中去若这个时候未找到!!! 此时就会从Top Chunk中分割一块出来

Top Chunk

Top Chunk之前我们看到过 它始终位于高地址处的地方 我们称为Top Chunk。它不属于任何一个bin中。当Bins中没有合适的chunk时 就需要到Top Chunk中需要合适的块了。 如果Top Chunk的大小满足用户所要求的chunk大小 那么会从Top Chunk中割下一块给用户作为User Chunk 另一块就作为Remainder Chunk 即缩小的Top Chunk存在着。 当Top Chunk的大小不满足用户所需的尺寸大小的时候,就需要增高对应的Top Chunk的高度 ,对于main_arena而言 是通过sbrk()系统调用拔高对应的Top Chunk的高度 即增大Top Chunk的大小以满足用户的需求而对于thread_arena而言 则是通过mmap系统调用 通过heap_info数据结构将多个堆串连在一起 以使得满足用户的需求!!! 虽然不在bins中 但会被记录在mstate中 即malloc_state结构中

Last Remainder Chunk

Last Remainder Chunk来源于最近一次的split操作 例如在用户所申请的chunk_size落入对应的Large Bins范围中 我们从对应的Large Bin中分出一块给用户,另一块放入到unsorted Bin中作为small bins和large bins的高速缓冲 之所以这样是因为保证小块可以被尽可能在虚拟地址上毗邻 方便之后free的时候进行合并操作。按照这个示例而言 会存在Remainder Chunk,而最近一次由于split而进入unsorted bin的就是 Last Remainder Chunk!!! 会被记录在malloc_state中 即mstate中
之后当用户再次请求small chunk的时候并且如果Last Remainder Chunk是Unsorted Bin中唯一的一块Chunk 那么 Last Remainder Chunk就会再次进行split操作 可以想象 按照这个样子 Last Remainder Chunk周围的都是small Chunk 当Free的时候就很可能会与周围一样free的small chunk 进行合并操作

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!