文章目录

  • ptmalloc
  • 设计假设
  • Arena
  • Chunk
  • Bins
  • 内存分配、释放流程
  • 总结

C++ STL : SGI-STL空间配置器源码剖析
Linux 内存管理 | 物理内存管理:物理内存、内存碎片、伙伴系统、slab分配器
Linux 内存管理 | 虚拟内存管理:虚拟内存空间、虚拟内存分配

在之前的几篇博客中,我曾经介绍过STL空间配置器、BuddySystem、Slab分配器等内存管理机制,也曾经简单的提及过linux用户态内存分配的策略,这一次就来深入讲一讲在用户态进行内存分配时,到底做了什么。


ptmalloc

在Linux中,当我们申请的内存小于128K时,malloc会使用sbrk或者brk区分配内存。而当我们申请大于128K的大块空间时,会使用mmap映射区进行分配。

但是由于上述的brk/sbrk/mmap都属于系统调用,因此当我们每次调用它们时,就会从用户态切换至内核态,在内核态完成内存分配后再返回用户态。

倘若每次申请内存都要因为系统调用而产生大量的CPU开销,那么性能会大打折扣。并且从上面的图我们也可以看出来,堆是从低地址往高地址增长,如果低地址的内存没有被释放,则高地址的内存就不能被回收,就会产生内存碎片的问题。

那么Linux中的malloc是如何实现解决这个问题的呢?

这就要提到大名鼎鼎的ptmalloc了,ptmalloc是glibc中默认的内存管理器。其底层采取了分箱式内存管理机制,也就是实现了一个类似哈希桶结构的内存池,当我们通过mallocfree申请和释放内存的时候,ptmalloc就会代替我们将这些内存进行管理,通过一系列的内存合并、申请策略,来让用户申请和释放内存的时候更加的高效且安全。

下面就来详细的介绍一下ptmalloc的工作原理


设计假设

ptmalloc在设计时集合了高效率、高空间利用率、高可用等目标,因此有了如下几种设计假设

  1. 具有长生命周期的大内存分配使用 mmap
  2. 特别大的内存分配总是使用 mmap
  3. 具有短生命周期的内存分配使用 brk,因为用 mmap 映射匿名页,当发生缺页异常
    时,linux 内核为缺页分配一个新物理页,并将该物理页清 0,一个 mmap 的内存块
    需要映射多个物理页,导致多次清 0 操作,很浪费系统资源,所以引入了 mmap
    分配阈值动态调整机制,保证在必要的情况下才使用 mmap 分配内存。
  4. 尽量只缓存临时使用的空闲小内存块,对大内存块或是长生命周期的大内存块在释
    放时都直接归还给操作系统。
  5. 对空闲的小内存块只会在 mallocfree 的时候进行合并,free 时空闲内存块可能放入 内存池中,不一定归还给操作系统。
  6. 收缩堆的条件是当前 free 的块大小加上前后能合并 chunk 的大小大于 64KB、,并且
    堆顶的大小达到阈值,才有可能收缩堆,把堆最顶端的空闲内存返回给操作系统。
  7. 需要保持长期存储的程序不适合用 ptmalloc 来管理内存。
  8. 为了支持多线程,多个线程可以从同一个分配区(arena)中分配内存,ptmalloc
    假设线程 A 释放掉一块内存后,线程 B 会申请类似大小的内存,但是 A 释放的内
    存跟 B 需要的内存不一定完全相等,可能有一个小的误差,就需要不停地对内存块
    作切割和合并,这个过程中可能产生内存碎片。

Arena

在老版本的glibc中使用的内存分配器是dlmalloc,其底层对于多线程的支持并不友好,因为所有线程共享同一个内存管理结构。所以当多个线程并发执行malloc时,为了保证线程安全,其通过使用互斥锁进行加锁,使得只能有一个线程能够访问临界资源,因此在并发环境下使用dlmalloc时会花费大量的时间在互斥锁的阻塞等待上,因此整个应用的效率极低。

而在ptmalloc中,为了解决多线程并发争抢锁的问题, 其设定了主分配区main_arean和非主分配区non_main_arena

  • 每个进程有一个主分配区,以及多个非主分配区
  • 主分配区可以使用brkmmap来分配空间,而非主分配区只能使用mmap
  • 非主分配区的数量只能增加,不能减少
  • 主分配区和非主分配区使用环形链表进行管理,并使用互斥锁保证线程安全

通过引入多个非主分配区,就可以将线程分发到不同的分配区中,将原先多个线程共享一个分配区变为了多个线程共享多个分配区,在一定程度上减轻了并发的压力。


Chunk

和其他内存管理机制一样,ptmalloc通过一个名为chunk的数据结构来管理和组织内存单元。为了节约内存,在使用时它的结构分为空闲的chuck使用中的chuck两个版本

使用中的chuck:

  • chuck指针指向chuck的起始地址,mem指针指向用户使用的内存块的起始地址,而next chunk则指向下一个chuck。
  • 在第二个域中,最低的一位p表示前一个chuck是否空闲,主要用于合并内存块的操作。当p=1时代表着上一次chunk正在使用,此时prev_size无效。p=0时代表前一个chuck空闲,prev_size有效。在ptmalloc分配第一个chuck时,总会将p置为1,防止程序越界访问。
  • M用于表示内存所处的区域,当M=1时为mmap映射区域分配,M=0为heap区域分配。
  • A用于标示分配区,A=1为非主分配区分配,A=0为主分配区分配。

空闲的chuck:

  • 空闲的chuck会被放到空闲的链表bins上,当用户申请内存时,其会先去bins上查找是否存在合适的内存。
  • 对于空闲的chuck,为了方便其在空闲链表上快速查找合适大小的chuck,他有指向上一个和下一个空闲chuck的指针,同时还有指向上一个和下一个空闲chuck的内存大小的指针。

Bins

对于空闲的shunk,ptmalloc采用分箱式内存管理,通过bins来维护这些空闲的chunk。ptmalloc一共维护了128个bin,同时每个bin中又维护了一个大小相近的chunk链表,如下图

根据chunk的大小以及状态不同,bins又分为以下四个种类

  • fast bins:fast bins是small bins的高速缓冲区,享有最快的内存分配、释放速度。当用户释放一块小于等于MAX_FAST(默认为64字节)的chunk时,会默认将其存放到fast bins中。当用户需要分配小内存时,会首先查看fast bins中是否存在合适的内存块,如果有则直接返回,如果没有才会去查看small bins上的空闲chunk。同时,ptmalloc会遍历fast bins,查看是否有合适的chunk需要合并,则将其合并后放入unsorted bin中
  • unsorted bin:unsorted bin只有一个,是bins的第一个位置。当用户释放的内存大雨MAX_FAST或者fast bins合并后的chunk都会进入unsorted bin上,当用户malloc的时候会先到unsorted bin上查找是否存在合适的bin,如果没有合适的bin,ptmalloc则会将unsorted bin伤的chunk放入bins上,然后再到bins上查找合适的空闲chunk。
  • small bins:用于存放固定大小的chunk,总共有64个bin,bin的大小从16字节开始,以8字节不断递增。当我们需要分配小内存块时,会采用精确匹配的方式从small bins中查找到合适的chunk。
  • large bins:用于存放大于512字节的空闲chunk,共有64个bin,bin按照顺序不定长升序排列,同时每个bin中的chunk按照大小降序排列

从上面我们可以得出以下结论

  • fast bins相当于small bins的cache,用于提高小内存的分配、释放效率,但也同时可能加剧内存碎片化
  • unsorted bin其实就是最近释放的内存集合,它会保留最近释放的chunk,从而消除了寻找合适的bin的时间开销,提高了内存分配和释放的效率
  • small bins和large bins可以合并相邻的空闲chunk,减轻了内存碎片化,但也同时降低了效率

当然,如果在上面的任何一个bin中都没办法找到合适的内存块,ptmalloc中还预留了其他的一些后备方式。

  • top chunk:在分配区中最顶部的chunk被称为top chunk,它不属于任何一个bin。当所有bin中都没有合适的内存时,就由它来响应用户请求。当用户申请的内存小于top chunk的内存时,其会将自己分割为两部分,一部分返回,另一部分则成为新的top chunk。如果用户申请的内存大于top chunk的内存,则top chunk会根据分配区的不同,分别调用sbrk或者mmap来进行扩容。
  • last remainder chunk:即最后一次small request中因分割而得到的剩余部分,它有利于改进局部性,当在small bins中找不到合适的chunk时,如果last remainded chunk的大小大于所需要的small chunk大小时,其就会将用户需要的那一部分分出去,剩余的部分则成为新的last remainded chunk,因此后续请求的chunk最终将被分配得彼此接近。
  • mmaped chunk:当分配的内存非常大的时候(大于128K),此时就需要通过mmap来申请内存,通过mmap申请的内存会被放到mmaped chunk上。同时,在释放的时候会通过判断chunk中的M是否为1来判断是否属于mmaped chunk,如果是则直接将内存交还给操作系统

内存分配、释放流程

分配流程:

  1. 获取分配区的锁
  2. 计算出需要分配内存的chunk大小
  3. 判断chunk的大小,如果小于MAX_FAST则到fast_bins上查找,如果小于512字节,则到small bins上查找
  4. 如果还没有找到,此时则进入unsorted bin上查找,如果找到合适的chunk并进行分配后还有剩余的空间,则根据其空间大小将其放入small bins或者large bins中。
  5. 进入large bins中查找,找到合适的bin后反向进行遍历,找到第一个大小满足的chunk进行分配,如果chunk分配完后还有剩余的空间,则将其放入unsorted bin中。
  6. 如果查找了所有的bin都没有找到合适的chunk,此时就需要操作top chunk来进行分配,如果满足大小小于top chunk,则切割top chunk来进行分配。如果大小大于top chunk,则此时会申请内存来满足分配需求。

释放流程:

  1. 获取分配区的锁
  2. 判断free的参数是否为空节点,如果是则什么都不做
  3. 判断当前chunk是否是mmap映射出的大块内存(M=1),如果是的话直接munmap释放掉
  4. 判断当前chunk是否和top chunk连续,如果连续则直接和top chunk合并
  5. 判断chunk大小是否大于MAX_FAST,如果大于则放入unsorted bin中,并检查是否能够进行合并,如果不能合并则直接free,如果能则进入步骤8
  6. 如果chunk大小小于MAX_FAST,则放入fast bins中,并判断是否有合并的情况,如果没有合并则free,如果有则进入下一步骤
  7. 在fast bin中,如果当前当前chunk能够进行合并,则将合并后的chunk放入unsorted bin中。
  8. 判断top chunk的大小是否大于mmap收缩阈值,如果满足则试图归还多出的那一部分给操作系统

总结

从上面的内容我们可以看出,ptmalloc虽然能够很好的进行内存管理,但是仍然存在着一系列的小问题,如:

  • 由于ptmalloc收缩内存是从top chunk开始,如果与top chunk相邻的chunk不能够释放,则top chunk以下的chunk都无法释放(即使后分配的内存先释放,也无法及时归还给系统)
  • 每个chunk至少为8字节(可能导致内存内碎片)
  • 虽然采用了主分配区+非主分配区的策略来优化多线程环境下的并发问题,但是在内存分配和释放的时候仍会首先进行加锁(影响性能)
  • 由于采用了非主分配区,导致内存不能在不同分配区间移动,内存使用不均衡(内存浪费)
  • 不定期分配长生命周期的内存(不利于回收,容易导致内存碎片)

所以后来各大厂商为了能够提升效率,开发出了如tcmallocjemalloc等内存分配器,但作为linux默认内存管理器的ptmalloc,以其稳定性始终占据着一席之地。

Linux glibc内存管理:用户态内存分配器——ptmalloc实现原理相关推荐

  1. 趣谈Linux操作系统学习笔记:用户态内存映射:如何找到正确的会议室?(第25讲)...

    一.mmap原理 在虚拟内存空间那一节,我们知道,每一个进程都有一个列表vm_area_struct,指向虚拟地址空间的不同内存块,这个变量名字叫mmap struct mm_struct {stru ...

  2. 【Linux 内核 内存管理】分区伙伴分配器 ② ( free_area 空闲区域结构体源码 | 分配标志位 | GFP_ZONE_TABLE 标志位区域类型映射表 |分配标志位对应的内存区域类型 )

    文章目录 一.free_area 空闲区域结构体源码分析 二.分配标志位 三.GFP_ZONE_TABLE 标志位区域类型映射表 四.分配标志位对应的内存区域类型 一.free_area 空闲区域结构 ...

  3. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  4. 属性与内存管理(属性与内存管理都是相互关联的)

    <span style="font-size:18px;"> 属性与内存管理(属性与内存管理都是相互关联的)第一部分一,属性:属性是OC2.0之后出来的新语法,用来取代 ...

  5. C++:内存管理:C++内存管理详解

    C++语言内存管理是指:对系统的分配.创建.使用这一系列操作.在内存管理中,由于是操作系统内存,使用不当会造成很麻烦的后果.本文将从系统内存的分配.创建出发,并且结合例子来说明内存管理不当会造成的结果 ...

  6. Unity 之 Mono内存管理与泄漏 — 内存是手游的硬伤(转)

    WeTest导读 内存是游戏的硬伤,如果没有做好内存的管理问题,游戏极有可能会出现卡顿,闪退等影响用户体验的现象.本文介绍了在腾讯游戏在Unity游戏开发过程中常见的Mono内存管理问题,并介绍了一系 ...

  7. JVM自动内存管理机制——Java内存区域(下)

    一.虚拟机参数配置 在上一篇<Java自动内存管理机制--Java内存区域(上)>中介绍了有关的基础知识,这一篇主要是通过一些示例来了解有关虚拟机参数的配置. 1.Java堆参数设置 a) ...

  8. Java内存管理:Java内存区域 JVM运行时数据区

    Java内存管理:Java内存区域 JVM运行时数据区 在前面的一些文章了解到javac编译的大体过程.Class文件结构.以及JVM字节码指令. 下面我们详细了解Java内存区域:先说明JVM规范定 ...

  9. 【C 语言必知必会】内存管理、动态分配内存、野指针

    C 语言内存管理.动态分配内存.野指针 文章目录 C 语言内存管理.动态分配内存.野指针 前言: 1.内存分区 1.1 代码区 1.2.1 全局初始化数据区(静态数据区data段) 1.2.2 未初始 ...

最新文章

  1. 隐式转换和显式转换及强制转换的区别
  2. 浅析如何才能提高网站的信息交互能力?
  3. 《JavaScript面向对象精要》——第1章 原始类型和引用类型1.1 什么是类型
  4. Hibernate -- Dao层 -- CURD -- 随记
  5. system类的 静态方法可以启动垃圾回收器。_Java—System类入门学习
  6. HiveJDBC与其他JDBC一起使用时出现java.lang.IllegalArgumentException: Bad URL format
  7. 桌面虚拟化在学生教室及机房的应用案例
  8. [转]URLRewriter使用通配符
  9. HardFault_Handler问题查找方法
  10. suse leap_Ruby程序检查leap年
  11. Spring Boot笔记-通过反射获取Spring Boot中的Bean(反射调用Service)
  12. easyui的tree获取父节点_通过DOM API 查找节点
  13. Java的System.out.println并不等于C的printf
  14. python实现水仙花数
  15. 我本人常去的一些技术性网站!!(不定期更新)
  16. ImageJ Merge荧光图片
  17. 微信,你的野心到底有多大?
  18. 2018今日头条内推笔试1
  19. 费马小定理 费马大定理 数论
  20. R语言七天入门教程二:认识变量与运算符

热门文章

  1. Nginx_反向代理配置讲解
  2. htc u11 android o,HTC U11+和HTC U11有什么区别?对比完秒懂 (全文)
  3. JDK1.8并发包中的类
  4. 第四单元用计算机写作,计算机复习题
  5. pytorch教程龙曲良16-20
  6. 【项目经验】如果想在mapper.xml文件中的一个标签中写多条sql语句,则需要在jdbc的配置文件中稍做配置
  7. 对于指针与malloc函数关系的研究,以及指针的赋值。
  8. zookeeper初探二 windows环境搭建伪集群
  9. 第一个冲刺期的第九天
  10. C# 该行已经属于还有一个表 的解决方法