写点什么

Linux c 开发 - 内存管理器 ptmalloc

用户头像
赖猫
关注
发布于: 2021 年 02 月 23 日

一、内存布局


了解 ptmalloc 内存管理器,就必须得先了解操作系统的内存布局方式。通过下面这个图,我很很清晰的可以看到堆、栈等的内存分布。


X86 平台 LINUX 进程内存布局:



上图就是 linux 操作系统的内存布局。内存从低到高分别展示了操作系统各个模块的内存分布。


  • Test Segment:存放程序代码,只读,编译的时候确定

  • Data Segment:存放程序运行的时候就能确定的数据,可读可写

  • BBS Segment:定义而没有初始化的全局变量和静态变量

  • Heap:堆。堆的内存地址由低到高。

  • Mmap:映射区域。

  • Stack:栈。编译器自动分配和释放。内存地址由高到低


二、ptmalloc 内存管理器


ptmalloc 是 glibc 默认的内存管理器。我们常用的 malloc 和 free 就是由 ptmalloc 内存管理器提供的基础内存分配函数。ptmalloc 有点像我们自己写的内存池,当我们通过 malloc 或者 free 函数来申请和释放内存的时候,ptmalloc 会将这些内存管理起来,并且通过一些策略来判断是否需要回收给操作系统。这样做的最大好处就是:让用户申请内存和释放内存的时候更加高效。


为了内存分配函数 malloc 的高效性,ptmalloc 会预先向操作系统申请一块内存供用户使用,并且 ptmalloc 会将已经使用的和空闲的内存管理起来;当用户需要销毁内存 free 的时候,ptmalloc 又会将回收的内存管理起来,根据实际情况是否回收给操作系统


1. 设计假设


ptmalloc 在设计时折中了高效率,高空间利用率,高可用性等设计目标。所以有了下面一些设计上的假设条件:


  • 具有长生命周期的大内存分配使用 mmap。

  • 特别大的内存分配总是使用 mmap。

  • 具有短生命周期的内存分配使用 brk。

  • 尽量只缓存临时使用的空闲小内存块,对大内存块或是长生命周期的大内存块在释放时都直接归还给操作系统。

  • 对空闲的小内存块只会在 malloc 和 free 的时候进行合并,free 时空闲内存块可能放入 pool 中,不一定归还给操作系统。

  • 收缩堆的条件是当前 free 的块大小加上前后能合并 chunk 的大小大于 64KB、,并且堆顶的大小达到阈值,才有可能收缩堆,把堆最顶端的空闲内存返回给操作系统。

  • 需要保持长期存储的程序不适合用 ptmalloc 来管理内存。

  • 不停的内存分配 ptmalloc 会对内存进行切割和合并,会导致一定的内存碎片


2. 主分配区和非主分配区


ptmalloc 的内存分配器中,为了解决多线程锁争夺问题,分为主分配区 main_area 和非主分配区 no_main_area。


  • 每个进程有一个主分配区,也可以允许有多个非主分配区。

  • 主分配区可以使用 brk 和 mmap 来分配,而非主分配区只能使用 mmap 来映射内存块

  • 非主分配区的数量一旦增加,则不会减少。

  • 主分配区和非主分配区形成一个环形链表进行管理。


3. chunk 内存块的基本组织单元


ptmalloc 通过 chunk 的数据结构来组织每个内存单元。当我们使用 malloc 分配得到一块内存的时候,这块内存就会通过 chunk 的形式被记录到 glibc 上并且管理起来。你可以把它想象成自己写内存池的时候的一个内存数据结构。


chunk 的结构可以分为使用中的 chunk 和空闲的 chunk


使用中的 chunk 和空闲的 chunk 数据结构基本项同,但是会有一些设计上的小技巧,巧妙的节省了内存。


使用中的 chunk:



  • chunk 指针指向 chunk 开始的地址;mem 指针指向用户内存块开始的地址。

  • p=0 时,表示前一个 chunk 为空闲,prev_size 才有效

  • p=1 时,表示前一个 chunk 正在使用,prev_size 无效 p 主要用于内存块的合并操作

  • ptmalloc 分配的第一个块总是将 p 设为 1, 以防止程序引用到不存在的区域

  • M=1 为 mmap 映射区域分配;M=0 为 heap 区域分配

  • A=1 为非主分区分配;A=0 为主分区分配


空闲的 chunk



  • 空闲的 chunk 会被放置到空闲的链表 bins 上。当用户申请内存 malloc 的时候,会先去查找空闲链表 bins 上是否有合适的内存。

  • fp 和 bp 分别指向前一个和后一个空闲链表上的 chunk

  • fp_nextsize 和 bp_nextsize 分别指向前一个空闲 chunk 和后一个空闲 chunk 的大小,主要用于在空闲链表上快速查找合适大小的 chunk。

  • fp、bp、fp_nextsize、bp_nextsize 的值都会存在原本的用户区域,这样就不需要专门为每个 chunk 准备单独的内存存储指针了。


空闲链表 bins


当用户使用 free 函数释放掉的内存,ptmalloc 并不会马上交还给操作系统(这边很多时候我们明明执行了 free 函数,但是进程内存并没有回收就是这个原因),而是被 ptmalloc 本身的空闲链表 bins 管理起来了,这样当下次进程需要 malloc 一块内存的时候,ptmalloc 就会从空闲的 bins 上寻找一块合适大小的内存块分配给用户使用。这样的好处可以避免频繁的系统调用,降低内存分配的开销。



ptmalloc 一共维护了 128bin。每个 bins 都维护了大小相近的双向链表的 chunk。


通过上图这个 bins 的列表就能看出,当用户调用 malloc 的时候,能很快找到用户需要分配的内存大小是否在维护的 bin 上,如果在某一个 bin 上,就可以通过双向链表去查找合适的 chunk 内存块给用户使用。


  • fast bins。fast bins 是 bins 的高速缓冲区,大约有 10 个定长队列。当用户释放一块不大于 max_fast(默认值 64)的 chunk(一般小内存)的时候,会默认会被放到 fast bins 上。当用户下次需要申请内存的时候首先会到 fast bins 上寻找是否有合适的 chunk,然后才会到 bins 上空闲的 chunk。ptmalloc 会遍历 fast bin,看是否有合适的 chunk 需要合并到 bins 上。

  • unsorted bin。是 bins 的一个缓冲区。当用户释放的内存大于 max_fast 或者 fast bins 合并后的 chunk 都会进入 unsorted bin 上。当用户 malloc 的时候,先会到 unsorted bin 上查找是否有合适的 bin,如果没有合适的 bin,ptmalloc 会将 unsorted bin 上的 chunk 放入 bins 上,然后到 bins 上查找合适的空闲 chunk。

  • small bins 和 large bins。small bins 和 large bins 是真正用来放置 chunk 双向链表的。每个 bin 之间相差 8 个字节,并且通过上面的这个列表,可以快速定位到合适大小的空闲 chunk。前 64 个为 small bins,定长;后 64 个为 large bins,非定长。

  • Top chunk。并不是所有的 chunk 都会被放到 bins 上。top chunk 相当于分配区的顶部空闲内存,当 bins 上都不能满足内存分配要求的时候,就会来 top chunk 上分配。

  • mmaped chunk。当分配的内存非常大(大于分配阀值,默认 128K)的时候,需要被 mmap 映射,则会放到 mmaped chunk 上,当释放 mmaped chunk 上的内存的时候会直接交还给操作系统。


4. 内存分配 malloc 流程


  • 获取分配区的锁,防止多线程冲突。

  • 计算出需要分配的内存的 chunk 实际大小。

  • 判断 chunk 的大小,如果小于 max_fast(64b),则取 fast bins 上去查询是否有适合的 chunk,如果有则分配结束。

  • chunk 大小是否小于 512B,如果是,则从 small bins 上去查找 chunk,如果有合适的,则分配结束。

  • 继续从 unsorted bins 上查找。如果 unsorted bins 上只有一个 chunk 并且大于待分配的 chunk,则进行切割,并且剩余的 chunk 继续扔回 unsorted bins;如果 unsorted bins 上有大小和待分配 chunk 相等的,则返回,并从 unsorted bins 删除;如果 unsorted bins 中的某一 chunk 大小 属于 small bins 的范围,则放入 small bins 的头部;如果 unsorted bins 中的某一 chunk 大小 属于 large bins 的范围,则找到合适的位置放入。

  • 从 large bins 中查找,找到链表头后,反向遍历此链表,直到找到第一个大小 大于待分配的 chunk,然后进行切割,如果有余下的,则放入 unsorted bin 中去,分配则结束。

  • 如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来进行分配了(top chunk 相当于分配区的剩余内存空间)。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 top chunk 中分出一块来。

  • 如果 top chunk 也不能满足需求,则需要扩大 top chunk。主分区上,如果分配的内存小于分配阀值(默认 128k),则直接使用 brk()分配一块内存;如果分配的内存大于分配阀值,则需要 mmap 来分配;非主分区上,则直接使用 mmap 来分配一块内存。通过 mmap 分配的内存,就会放入 mmap chunk 上,mmap chunk 上的内存会直接回收给操作系统。


5. 内存释放 free 流程


  • 获取分配区的锁,保证线程安全。

  • 如果 free 的是空指针,则返回,什么都不做。

  • 判断当前 chunk 是否是 mmap 映射区域映射的内存,如果是,则直接 munmap()释放这块内存。前面的已使用 chunk 的数据结构中,我们可以看到有 M 来标识是否是 mmap 映射的内存。

  • 判断 chunk 是否与 top chunk 相邻,如果相邻,则直接和 top chunk 合并(和 top chunk 相邻相当于和分配区中的空闲内存块相邻)。转到步骤 8

  • 如果 chunk 的大小大于 max_fast(64b),则放入 unsorted bin,并且检查是否有合并,有合并情况并且和 top chunk 相邻,则转到步骤 8;没有合并情况则 free。

  • 如果 chunk 的大小小于 max_fast(64b),则直接放入 fast bin,fast bin 并没有改变 chunk 的状态。没有合并情况,则 free;有合并情况,转到步骤 7

  • 在 fast bin,如果当前 chunk 的下一个 chunk 也是空闲的,则将这两个 chunk 合并,放入 unsorted bin 上面。合并后的大小如果大于 64KB,会触发进行 fast bins 的合并操作,fast bins 中的 chunk 将被遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中,fast bin 会变为空。合并后的 chunk 和 topchunk 相邻,则会合并到 topchunk 中。转到步骤 8

  • 判断 top chunk 的大小是否大于 mmap 收缩阈值(默认为 128KB),如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。free 结束。


6. mallopt 参数调优


  • M_MXFAST:用于设置 fast bins 中保存的 chunk 的最大大小,默认值为 64B。最大 80B

  • M_TRIM_THRESHOLD:用于设置 mmap 收缩阈值,默认值为 128KB。

  • M_MMAP_THRESHOLD:M_MMAP_THRESHOLD 用于设置 mmap 分配阈值,默认值为 128KB。当用户需要分配的内存大于 mmap 分配阈值,ptmalloc 的 malloc()函数其实相当于 mmap()的简单封装,free 函数相当于 munmap()的简单封装。

  • M_MMAP_MAX:M_MMAP_MAX 用于设置进程中用 mmap 分配的内存块的地址段数量,默认值为 65536

  • M_TOP_PAD:该参数决定了,当 libc 内存管理器调用 brk 释放内存时,堆顶还需要保留的空闲内存数量。该值缺省为 0.


7. 使用注意事项


为了避免 Glibc 内存暴增,需要注意:


  • 后分配的内存先释放,因为 ptmalloc 收缩内存是从 top chunk 开始,如果与 top chunk 相邻的 chunk 不能释放,top chunk 以下的 chunk 都无法释放

  • Ptmalloc 不适合用于管理长生命周期的内存,特别是持续不定期分配和释放长生命周期的内存,这将导致 ptmalloc 内存暴增。

  • 多线程分阶段执行的程序不适合用 ptmalloc,这种程序的内存更适合用内存池管理

  • 尽量减少程序的线程数量和避免频繁分配/释放内存。频繁分配,会导致锁的竞争,最终导致非主分配区增加,内存碎片增高,并且性能降低。

  • 防止内存泄露,ptmalloc 对内存泄露是相当敏感的,根据它的内存收缩机制,如果与 top chunk 相邻的那个 chunk 没有回收,将导致 top chunk 一下很多的空闲内存都无法返回给操作系统。

  • 防止程序分配过多内存,或是由于 Glibc 内存暴增,导致系统内存耗尽,程序因 OOM 被系统杀掉。预估程序可以使用的最大物理内存大小,配置系统的/proc/sys/vm/overcommit_memory,/proc/sys/vm/overcommit_ratio,以及使用 ulimt –v 限制程序能使用虚拟内存空间大小,防止程序因 OOM 被杀掉。


Linux、C/C++技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、和热门技术教学视频资料共享在里面(包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK 等等.),有需要的可以自行添加哦!~



以上不足的地方欢迎指出讨论。


原文链接:https://blog.csdn.net/initphp/article/details/50833036


用户头像

赖猫

关注

还未添加个人签名 2020.11.28 加入

纸上得来终觉浅,绝知此事要躬行

评论

发布
暂无评论
Linux c 开发 - 内存管理器ptmalloc