linux堆内存管理

堆内存的使用在linux开发过程中非常普遍,我们有必要了解相关的内存管理方便我们对内存问题的理解和定位。

堆内存结构层次

linux的堆内存管理分为三个层次,分别为分配区area、堆heap和内存块chunk。

  • area:堆内存最上层即为分配区area。分配区area分为主分配区(main area)和线程分配区(thread area)。
  1. 主进程堆空间对应的分配区即为主分配区main area,每个进程仅有一个主分配区,对应我们通常所说的bss段上面堆空间位置。
  2. 线程堆空间对应的分配区即为线程分配区thread area。理论上每个线程对应一个线程分配区,但避免资源的浪费,线程分配区也有数量上限值,一般有如下关系:
    —32位系统:2 * CPU核数量
    —64位系统:8 * CPU核数量
    当分配区数量达到上限时,将查找一个已有的满足分配需求的可用分配区,否则会阻塞直至能获取满足要求的分配区。因此,最后可能出现几个线程共用一个分配区的情况。
    分配区area对应的数据结构为malloc_state,如下:
    malloc_state
  • heap:每个分配区下最少有一个堆。若当前的堆分配区空闲空间不够满足分配需求且请求内存大小小于128K时,需要对当前的分配区进行扩展,但对于主分配区和线程分配区来说扩容的方式不一样,具体如下:
分配区(malloc_state)堆区数量(heap_info)扩容方式
主分配区(main area)一个调用brk系统调用扩展堆的上限
线程分配区(thread area)多个调用mmap分配新的文件映射区域,并将该映射区连接到当前的堆区

堆区对应的数据结构heap_info具体如下:
heap_info

  • chunk:每次glibc分配的内存块称为chunk。malloc_chunk的数据结构如下:
    malloc_chunk
    malloc_chunk数据结构的使用分析可参考:https://blog.csdn.net/sy4331/article/details/120470932
    内存管理结构如下:
    内存管理结构图
    main area/thread area数据结构空间分布:
    main area/thread area数据结构

空闲内存管理

我们通过free释放的内存一般不会立马释放给内核,而是本地缓存起来,这样下次再申请相同大小内存时可以直接分配,不用再陷入内核,这大大提高了内存申请、释放效率。
为了进一步提高空闲内存的管理效率,glibc将空闲内存分为top chunk和bin chunk。

  • top chunk:指堆顶(内存的最高地址)的那一片连续内存。该内存主要用于分配大内存需求,当空闲的内存块都不满足分配需求时,可扩展top chunk。
  • bin:已分配然后被回收的内存,是分散在已使用内存中的内存碎片。bin是包括一类具有相同特点的内存chunk块集合。
    空闲内存分类
    空闲内存分类如上图所示。bin中根据管理的内存大小及用途,又分为fast bins、unsorted bin、small bins和large bins。

fast bins

fast bins具有最快的申请和释放速度,其对应分配区area数据结构malloc_state中fastbinsY数组。该数组为结构体指针类型,其实为一个个单向链表,每一个数组成员指向链表头。
malloc_state中的fast bins:
fast bins

特点:

  1. 每一个单向链表指向的内存chunk大小均相等,即同一个fast bin下的内存块大小相等。
  2. 相邻的fast bin的内存块chunk大小相差8字节。
  3. fast bins可存放16、24、32、40、48、56、64、72、80字节大小内存块(包括malloc_chunk的头结构)。
  4. 释放后的内存块chunk的各类标志位,如PRE_INUSE等均不会改变,相邻的chunk也不会合并。

unsorted bin

当内存块chunk被释放后且大小不在fast bins范围内,则该内存块将被保存在unsorted bin中。unsorted bin中的内存块没有大小限制,且是无序的。unsorted bin采用的是一个单链表连接所有的内存,对应malloc_state中的bins中的bins[1]
unsorted bin

small bins

small bins中保存的是大小小于512字节的空闲内存块chunk,其具体放在malloc_state中的bins结构体指针数组中的bins[2]-bins[63]。

特点:

  1. 每一个大小内存的chunk均采用双向链表表示。如bins[2]、bins[3]分别指向16字节大小chunk bins链表的头部和尾部。
  2. 每一个双向链表指向的内存chunk大小均相等,即同一个small bin下的内存块大小相等。
  3. 相邻的fast bin的内存块chunk大小相差8字节。
  4. fast bins可存放16、24、32、40、48、…、504字节大小内存块(包括malloc_chunk的头结构)。

从上面特点可知,fast bins和small bins属性较为相似,fast bins可以看做small bins的一个高速缓存,small bins保存的内存块大小范围更广。

large bins

large bins中保存的是大小大于等于512字节的内存块,对应保存在malloc_state中结构体指针数组bins中的bins[64]-bins[126]。

特点:

  1. 每一类内存的bins均采用双向链表表示。如bins[64]、bins[65]分别指向512字节大小chunk bins链表的头部和尾部。
  2. 每一个双向链表指向的内存chunk大小均不相等,从链表头部向尾部依次递减。
  3. 相邻的large bin的内存块chunk大小相差不相等,且bins数组下标越大(即bins内存越大),相邻large bin的内存块chunk相差越来越大。
  4. fast bins保存大于等于512字节大小内存块(包括malloc_chunk的头结构)。

上述几类空闲内存bins特点总结如下;
malloc_state数据结构分析

内存申请流程

内存分配时,查找空闲内存的优先级从高到低为fast bins、small bins、unsorted bins、large bins和top chunk,查找时从高优先级到低优先级一次查找满足需求的内存,直至找到为止。
查找空闲内存优先级顺序:
查找空闲内存优先级顺序
内存申请的具体流程如下:

  1. 首先将用户申请的内存长度转化为需要分配的内存块chunk长度chunk_size,这里面需要将用户请求长度加上malloc_chunk的前两个成员uchunk_prev_size和mchunk_size所占用空间(32位系统下占用8字节)。此外,还需要考虑内存对齐问题。对于32位系统,8字节对齐;64位系统,16字节对齐。这里面还涉及最小分配长度问题。32位系统最小分配长度为16字节;64位系统最小分配长度为32字节。当我们在32位系统下调用malloc(0),glibc也会分配16字节内存。
系统位数内存对齐长度最小分配长度
32位8字节16字节
64位16字节24字节
  1. 判断需要分配的内存块长度chunk_size是否小于等于fast bins管理内存的最大长度(初始时fast bins管理内存最大长度为64字节,后面为80字节)。当在范围内,则在fast bins里面查找满足需求的内存。若找到,则返回用户内存,结束;否则转到步骤3。
  2. 判断chunk_size是否在small bins范围内(<512字节)。若在范围内,则查找small bins。若找到满足需求内存,则返回用户内存,结束;否则跳转到步骤4.
  3. 遍历fast bins,合并相邻内存(保证虚拟地址连续)。将合并后的内存块chunk从fast bins中剥离并加入到unsorted bins。
  4. 遍历查找unsorted bins。若当前内存块不满足用户需求,则将该内存块根据其大小放到small bins或者large bins;若找到,则返回用户内存,结束。重复步骤5,直至找到满足需求内存或者遍历完也未找到,则跳转到步骤6。
  5. 遍历large bins。根据“small first,best fit”原则,找到最符合需求的内存块。当找到该内存块,若该内存块可以分割(剩余内存块不小于最小内存块大小),则分割该内存块,将一部分返回给用户,剩余部分放到unsorted bins中;若不能分割,则直接将该内存块返回给用户。若遍历完large bins也没有找到,则跳转到步骤7。
  6. 经过上述步骤后还没有找到满足需求的内存块,则表明fast bins、small bins、large bins均没有符合要求内存块,此时只能查看top chunk了。若top chunk当前大小能满足要求,则分割top chunk,将低地址返回给用户,高地址作为新的top chunk;否则,跳转到步骤8。
  7. 判断chunk_size是否已超过mmap分配阈值(128k)。若大于mmap分配阈值,则直接调用mmap分配需求内存空间。否则只能扩展堆区从而增大top chunk了,跳转到步骤9。
  8. 扩展堆区对于main area和thread area不同。若为main area,则通过调用sbrk系统调用向内核申请扩展堆顶以扩大堆空间;若为thread area,则通过mmap系统调用申请新的内存映射区,并将新的内存映射区添加到当前的分配区,同时,调整top chunk指向新分配的内存映射区,同时将老的top chunk改为普通的free chunk从而扩展堆空间 。通过上面操作,top chunk肯定能满足用户分配需求了,则跳转到步骤7,再次通过top chunk分配满足用户需求的内存块。
    glibc内存申请流程图如下:
    glibc内存申请流程

内存释放流程

内存释放流程相对简单,具体流程如下:

  1. 根据用户释放的内存地址获取对应的内存块chunk大小。
  2. 释放的内存块大小是否在fast bins范围内。若在,则将释放的内存放到fast bins中,结束;否则,跳转到步骤3。
  3. 判断释放的内存是否通过mmap申请的。若是,则直接通过unmmap释放该内存,结束;否则,跳转到步骤4。
  4. 判断释放内存块的前一个内存块是否为空闲内存(连续地址的低地址侧)。若是,则合并该空闲内存。
  5. 判断释放内存块的后一个内存块是否为空闲内存(连续地址的高地址侧)。若不是,则直接将内存块放到unsorted bin,结束;否则进一步看该内存块是否为top chunk。若为top chunk,则将待释放内存块合并到top chunk,并跳转到步骤6;否则,合并后一个空闲内存块,并将合并后的内存块放到unsorted bin中,结束。
  6. 判断当前分配区area是否为main area。若是,则跳转到步骤7;否则,跳转到步骤8。
  7. 判断当前的top chunk的大小是否超过了内存回收阈值(128k)。若是,则通过brk系统调用释放top chunk一部分内存给内核;否则,结束。
  8. 判断当前的top chunk所在的堆区内存(即heap_info数据结构管理对应的内存)是否已经全部释放了。若是,则调用unmmap释放该整个堆区(heap_info),将该部分内存返回给内核;否则,结束。
    glibc内存释放流程图如下:
    glibc内存释放流程图
    通过上述分析可知,我们通常调用free接口释放的内存最终有以下几个去处:
    1. fast bins
    2. unsorted bin
    3. top chunk
    4. 通过unmmap释放,将内存返回给内核。

注意,在释放内存时,若该内存最终是放到了fast bins中,为了提高下次分配效率,相邻空闲内存块不会合并,且该内存块和相邻的后一个内存块的头部结构malloc_chunk中的P(PREV_INUSE)、M(IS_MMAPPED)、N(NON_MAIN_AREA)均不会改变,维持原状。但若放在其他处,如unsorted bin、top chunk中,该内存块和相邻的后一个内存块对应的malloc_chunk中的P、M、N标志位均会做相应调整,特别是相邻的后一个内存块的P(PREV_INUSE)标志位会置为0,表示刚刚释放的内存块现处于空闲状态。

参考:https://blog.csdn.net/m0_37765662/article/details/119078410
https://blog.csdn.net/maokelong95/article/details/51989081?spm=1001.2014.3001.5501
https://blog.csdn.net/maokelong95/article/details/52006379?spm=1001.2014.3001.5501

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值