Linux进程内存管理概述

42 篇文章 0 订阅

内存管理方法总结

可用于内存管理的方法有许多种,它们各有好处与不足,不同的内存管理方法有各自最适用的情形, 通常有四类常用方法

C 风格的内存管理程序

主要实现malloc(), free()函数, 通过调用brk()或者mmap()给进程添加虚拟内存
C风格的内存管理程序包括: Doug Lea Malloc,ptmalloc,BSD malloc,Hoard,TCMalloc
缺点: 因为常常不能得知释放时机, malloc()难以对生存周期跨函数的内存进行管理

池式内存管理

一种半内存管理方法, 通过帮助程序进行自动内存管理. 比如Apache采用池式内存管理, 将连接拆分为不同阶段, 内个阶段都有各自的内存池, 在每个阶段结束会释放所有内存.
优点

  • 应用程序可以简单地管理内存
  • 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1)时间内完成,释放内存池所需时间也差不多(其实是 O(n)时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))
  • 可以预先分配错误处理池(Error-handling pools), 以便程序在常规内存被耗尽时仍
    可以恢复
  • 有非常易于使用的标准实现

缺点

  • 内存池只适用于操作可以分阶段的程序
  • 内存池通常不能与第三方库很好地合作
  • 如果程序的结构发生变化,则不得不修改内存池,可能需要重新设计内存管理系统
  • 必须记住需要从哪个池进行分配. 如果在这里出错,就很难捕获该内存池
引用计数

java, perl, python等语言采用引用计数的形式进行内存管理, 所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数, 引用次数由语言自动处理, 除了另外实现扩展模块, 不需要coder处理, 极大提高编程的安全性和方便性.
优势

  • 实现简单
  • 易于使用
  • 由于引用是数据结构的一部分,有一个好的缓存位置

劣势

  • 要求记得调用引用计数函数
  • 无法释放作为循环数据结构的一部分的结构
  • 减缓每一个指针的分配
  • 所使用的对象采用引用计数,但是当使用异常处理(如 try 或 setjmp()/ longjmp())时,必须采取其他方法。
  • 需要额外的内存来处理引用
  • 引用计数占用了结构中的第一个位置,占用最快访问的位置
  • 在多线程环境中性能下降明显
垃圾收集

垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据——栈数据、全局变量、寄存器——作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。
优点

  • 不存在内存双重释放或对象的生命周期超预期
  • 使用某些收集器,可以使用与常规分配相同的 API

缺点

  • 使用大部分收集器时,无法干涉何时释放内存
  • 在多数情况下,垃圾收集比其他形式的内存管理更慢
  • 垃圾收集错误引发的缺陷难于调试
  • 如果忘记将不再使用的指针设置为 null,仍然存在内存泄漏


内存管理器设计目标

设计内存管理器时需要考虑8个目标
(1) 最大化兼容性
要实现内存管理器时, 先要定义出分配器的接口函数. 接口函数要遵循现有标准, 如POSIX 或 Win32

(2) 最大化可移植性
通常内存管理器要向 OS 申请内存,然后进行二次分配。在适当的时候要扩展内存或释放多余的内存,需要调用 OS 提供的函数。OS 提供的函数则因平台而异,要求尽量抽象出平台相关的代码保证内存管理器的可移植性。

(3) 浪费最小的空间
内存管理器要管理内存要使用自己定义的数据结构,这些数据结构本身也要占内存空间。在用户眼中,这些内存空间毫无疑问是浪费掉了,故不能浪费内存太多在内存管理器本身; 另外, 内存碎片也会浪费空间, 若内存管理器中有大量的内存碎片,它们是一些不连续的小块内存,它们总量可能很大,但无法使用, 所以设计内存管理器时需要考虑内存碎片问题.

(4) 最快的速度
内存分配/释放是常用的操作。按着 2/8 原则,常用的操作就是性能热点,热点函数的性能对系统的整体性能尤为重要, 需要优化热点函数的性能.

(5) 最大化可调性
内存管理算法设计的难点就在于要适应用不同的情况。事实上,如果缺乏应用的上下文,
是无法评估内存管理算法的好坏的。可以说在任何情况下,专用算法都比通用算法在时空性能上的表现更优。现实不需要追求最优算法,代价太高,能达到次优就行。设计一套通用内存管理算法,通过一些参数对它进行配置,可以让它在特定情况也有相当出色的表现,这就是可调性。

(6) 最大化局部性
使用 cache 可以提高程度的速度,拿 CPU 内部的 cache 和 RAM 的访问速度相比,速度可能相差一个数量级。两者的速度上的差异固然重要,但这并不是提高速度的充分条件,只是必要条件。另外一个条件是程序访问内存的局部性(Locality)。大多数情况下,程序总访问一块内存附近的内存,把附近的内存先加入到 cache 中,下次访问 cache 中的数据,速度就会提高。因此,内存管理算法要考虑这一因素,减少 cache miss 和 page fault。

(7) 最大化调试功能
内存管理器提供的调试功能,需要强大易用,特别对于嵌入式环境,内存错误检测工具缺乏,内存管理器提供的调试功能就更是不可或缺。

(8) 最大化适应性
联系之前的最大化可调性,让内存管理器适用于不同的情况。但对于不同情况都要去调设置,无疑太麻烦,是非用户友好的。要尽量让内存管理器适用于很广的情况,只有极少情况下才去调设置。设计是一个多目标优化的过程,有些目标之间存在着竞争。需要平衡这些竞争力是设计的难点之一。



Ptmalloc 内存管理概述

简介

Linux 中 malloc 的早期版本是由 Doug Lea 实现的,它有一个重要问题就是在并行处理时多个线程共享进程的内存空间,各线程可能并发请求内存,在这种情况下应该如何保证分配和回收的正确和高效。Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支持多线程——ptmalloc,在glibc-2.3.x.中已经集成了ptmalloc2,即我们平时使用的malloc,目前 ptmalloc 的最新版本 ptmalloc3。ptmalloc2 的性能略微比 ptmalloc3 要高一点点。ptmalloc 实现了 malloc(),free()以及一组其它的函数. 以提供动态内存管理的支持。分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序,为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存,并通过算法管理这块内存, 来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。
分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。为实现一个高效的分配器,需要考虑很多的因素, 包括分配器本身管理内存块所占用的内存空间必须很小,以及分配算法必须要足够的快。

设计假设

Ptmalloc 在设计时折中了高效率,高空间利用率,高可用性等设计目标。在其实现代码
中,隐藏着内存管理中的一些设计假设,由于某些设计假设,导致了在某些情况下 ptmalloc的行为很诡异。设计假设包括:

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

(1) main_arena与non_main_arena
在 Doug Lea 实现的内存分配器中只有一个主分配区(main arena),每次分配内存都必须对主分配区加锁,分配完成后释放锁,在 SMP 多线程环境下,对主分配区的锁的争用很激烈,严重影响了 malloc 的分配效率。于是 Wolfram Gloger 在 Doug Lea 的基础上改进使得Glibc 的 malloc 可以支持多线程,增加了非主分配区(non main arena)支持,主分配区与非主分配区用环形链表进行管理。每一个分配区利用互斥锁(mutex)使线程对于该分配区的访问互斥。
每个进程只有一个主分配区,但可能存在多个非主分配区,ptmalloc 根据系统对分配区的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少。主分配区可以访问进程的 heap 区域和 mmap 映射区域,也就是说主分配区可以使用 sbrk 和 mmap向操作系统申请虚拟内存。而非主分配区只能访问进程的 mmap 映射区域,非主分配区每次使用 mmap()向操作系统“批发”HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统默 认为 64MB)大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售”出去。所以 ptmalloc 在必要的情况下才会调用 mmap()函数向操作系统申请虚拟内存。
主分配区可以访问 heap 区域,如果用户不调用 brk()或是 sbrk()函数,分配程序就可以保证分配到连续的虚拟地址空间,因为每个进程只有一个主分配区使用 sbrk()分配 heap 区域的虚拟内存。主分配区可以访问 heap 区域,如果用户不调用 brk()或是 sbrk()函数,分配程序就可以保证分配到连续的虚拟地址空间,因为每个进程只有一个主分配区使用 sbrk()分配 heap 区域的虚拟内存。如果主分配区的内存是通过 mmap()向系统分配的,当 free 该内存时,主分配区会直接调用 munmap()将该内存归还给系统。
当某一线程需要调用 malloc()分配内存空间时,该线程先查看线程私有变量中是否已经存在一个分配区,如果存在,尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存, 如果失败,该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经加锁,那么 malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。

(2) chunk
用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统,它们也会被表示为一个 chunk,ptmalloc 使用特定的数据结构来管理这些空闲的 chunk。
使用中的chunk示意图
在这里插入图片描述
chunk 指针指向一个 chunk 的开始,一个 chunk 中包含了用户请求的内存区域和相关的控制信息。图中的 mem 指针才是真正返回给用户的内存指针。chunk 的第二个域的最低一位为 P,它表示前一个块是否在使用中,P 为 0 则表示前一个 chunk 为空闲,这时chunk 的第一个域 prev_size 才有效,prev_size 表示前一个 chunk 的 size,程序可以使用这个值来找到前一个 chunk 的开始地址。当 P 为 1 时,表示前一个 chunk 正在使用中,prev_size无效,程序也就不可以得到前一个chunk的大小。不能对前一个chunk进行任何操作。ptmalloc分配的第一个块总是将 P 设为 1,以防止程序引用到不存在的区域。
Chunk 的第二个域的倒数第二个位为 M,他表示当前 chunk 是从哪个内存区域获得的虚拟内存。M 为 1 表示该 chunk 是从 mmap 映射区域分配的,否则是从 heap 区域分配的。
Chunk 的第二个域倒数第三个位为 A,表示该 chunk 属于主分配区或者非主分配区,如果属于非主分配区,将该位置为 1,否则置为 0。

空闲中的chunk则不同:
在这里插入图片描述
当 chunk 空闲时,其 M 状态不存在,只有 AP 状态,原本是用户数据区的地方存储了四个指针,指针 fd 指向后一个空闲的 chunk,而 bk 指向前一个空闲的 chunk,ptmalloc 通过这两个指针将大小相近的 chunk 连成一个双向链表。对于 large bin 中的空闲 chunk,还有两个指针,fd_nextsize 和 bk_nextsize,这两个指针用于加快在 large bin 中查找最近匹配的空闲chunk。不同的 chunk 链表又是通过 bins 来组织的

关于chunk的空间复用
为了使得 chunk 所占用的空间最小,ptmalloc 使用了空间复用,一个 chunk 或者正在被使用,或者已经被 free 掉,所以 chunk 的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。
32 位系统下,空闲时,一个 chunk 中至少需要 4 个 size_t( == 4B)大小的空间,用来存储 prev_size,size,fd 和 bk ,也就是 16B,chunk 的大小要对齐到 8B(64位系统下对齐16B, 按字长双倍对齐)。当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size域无效。所以这个空间也可以被当前 chunk 使用。故实际上,一个使用中的 chunk 的大小的计算公式应该是:in_use_size = (用户请求大小+ 8 - 4 ) align to 8B,这里加 8 是因为需要存储 prev_size 和 size,但又因为向下一个 chunk“借”了 4B,所以要减去 4。最后,因为空闲的 chunk 和使用中的chunk 使用的是同一块空间。所以肯定要取其中最大者作为实际的分配空间。即最终的分配空间 chunk_size = max(in_use_size, 16)。一般malloc分配的chunk指的都是这个经过转换的实际需要分配的内存大小,而不是用户请求的内存分配大小。可以看成一个chunk仅有size域, men数据域组成, prev_size域是与前/后紧邻的chunk共用的

总结起来, 复用的域包括prev_size, mem开始的前8个字节, 存储fd, bk, 如果是large bins则还需要8字节存fd_nextsize, bk_nextsize, 标志位AMP中M在使用和空闲时有差异, 空闲时不需要通过M区分heap段还是mmap段

特殊的chunk

不按照一般意义的chunk来组织, 一共有三种特殊形式的chunk

Top chunk
top chunk 对于主分配区和非主分配区不一样。
对于非主分配区会预先从 mmap 区域分配一块较大的空闲内存模拟 sub-heap,通过管理 sub-heap 来响应用户的需求,因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲 chunk,叫做 top chunk。当 bins 和 fast bins 都不能满足分配需要的时候,ptmalloc 会设法在 top chunk 中分出一块内存给用户,如果 top chunk 本身不够大,分配程序会重新分配一个 sub-heap,并将 top chunk 迁移到新的 sub-heap 上,新的 sub-heap与已有的 sub-heap 用单向链表连接起来,然后在新的 top chunk 上分配所需的内存以满足分配的需要,实际上,top chunk 在分配时总是在 fast bins 和 bins 之后被考虑,所以,不论 top chunk 有多大,它都不会被放到 fast bins 或者是 bins 中。Top chunk 的大小是随着分配和回收不停变换的,如果从 top chunk 分配内存会导致 top chunk 减小,如果回收的 chunk 恰好与 top chunk 相邻,那么这两个 chunk 就会合并成新的 top chunk,从而使 top chunk 变大。如果在 free 时回收的内存大于某个阈值,并且 top chunk 的大小也超过了收缩阈值,ptmalloc会收缩 sub-heap,如果 top-chunk 包含了整个 sub-heap,ptmalloc 会调用 munmap 把整个sub-heap 的内存返回给操作系统。
由于主分配区是唯一能够映射进程 heap 区域的分配区,它可以通过 sbrk()来增大或是收缩进程 heap 的大小,ptmalloc 在开始时会预先分配一块较大的空闲内存(heap),主分配区的 top chunk 在第一次调用 malloc 时会分配一块(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap,用户从 top chunk 分配内存时,可以直接取出一块内存给用户。在回收内存时,回收的内存恰好与 top chunk 相邻则合并成新的 top chunk,当该次回收的空闲内存大小达到某个阈值,并且 top chunk 的大小也超过了收缩阈值,会执行内存收缩,减小 top chunk 的大小,但至少要保留一个页大小的空闲内存,从而把内存归还给操作系统。如果向主分配区的 top chunk 申请内存,而 top chunk 中没有空闲内存,ptmalloc会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。

mmaped chunk
当需要分配的 chunk 足够大,而且 fast bins 和 bins 都不能满足要求,甚至 top chunk 本身也不能满足分配需求时,ptmalloc 会使用 mmap 来直接使用内存映射来将页映射到进程空间。这样分配的 chunk 在被 free 时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致 segmentation fault 错误。这样的 chunk 不包含在任何bin中, free时就直接释放.

Last remainder
Last remainder 同 top chunk 和 mmaped chunk 一样,不会在任何 bins 中找到这种 chunk。当需要分配一个 small chunk,但在 small bins 中找不到合适的 chunk,如果 last remainder chunk 的大小大于所需的 small chunk 大小,last remainder chunk被分裂成两个 chunk,其中一个 chunk 返回给用户,另一个 chunk 变成新的 last remainder chuk。

(3) bins
用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会首先试图在空闲的chunk 中挑选一块给用户,这样避免频繁的系统调用,降低内存分配的开销。ptmalloc将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共维护了 128 个 bin,并使用一个数组来存储这些bin
在这里插入图片描述

数组中的第一个为 unsorted bin,数组中从 2 开始编号的前 64 个 bin 称为 small bins,Small bins 后面的 bin 被称作 large bins。此外不在数组中的有fast bins, 作为提高系统分配速度的数据结构而维护.
同一个small bin中的chunk具有相同的大小。两个相邻的small bin中的chunk大小相差8bytes.small bins 中的 chunk 按照最近使用顺序进行排列,最后释放的 chunk 被链接到链表的头部,而申请 chunk 是从链表尾部开始.
large bins 中的每一个 bin 分别包含了一个给定范围内的 chunk,其中的 chunk 按大小序排列。相同大小的 chunk 同样按照最近使用顺序排列。ptmalloc 使用“smallest-first,best-fit”原则在空闲 large bins 中查找合适的 chunk。
当空闲的 chunk 被链接到 bin 中的时候,ptmalloc 会把表示该 chunk 是否处于使用中的
标志 P 设为 0(注意,这个标志实际上处在地址相邻的下一个 chunk 中),同时 ptmalloc 还会检查它前后的 chunk 是否也是空闲的,如果是的话,ptmalloc 会首先把它们合并为一个大的 chunk,然后将合并后的 chunk 放到 unstored bin 中。要注意的是,并不是所有的 chunk 被释放后就立即被放到 bin 中。ptmalloc 为了提高分配的速度,会把一些小的的 chunk 先放到一个叫做fast bins 的容器内.

Fast Bins
一般的情况, 程序在运行时经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,可能立即就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样比较低效,故 ptmalloc 中在分配过程中引入了 fast bins,不大于 max_fast (默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins 中,fast bins 中的 chunk 并不改变它的使用标志 P。这样也就无法将它们合并,当需要给用户分配的 chunk 小于或等于 max_fast 时,ptmalloc 首先会在 fast bins 中查找相应的空闲块,然后才会去查找bins中的空闲chunk。在某个特定的时候,ptmalloc会遍历fast bins中的chunk将相邻的空闲 chunk 进行合并,并将合并后的 chunk 加入 unsorted bin 中,然后再将 usorted bin 里的 chunk 加入 bins 中.

Unsorted Bin
unsorted bin 的队列在 bins 数组的第一个,如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中,在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则 ptmalloc 会先在 unsorted bin 中查找合适的空闲 chunk,然后才查找 bins。如果 unsorted bin 不能满足分配要求。malloc便会将 unsorted bin 中的 chunk 加入 bins 中。然后再从 bins 中继续进行查找和分配过程。unsorted bin 可以看做是 bins 的一个缓冲区,可以加快分配的速度。

(4) sbrk与mmap
.bss 段之上的这块分配给用户程序的空间被称为 heap. start_brk 指向 heap 的开始,brk 指向 heap 的顶部。可以使用系统调用 brk()和 sbrk()来增加标识 heap 顶部的 brk 值,从而线性的增加分配给用户的 heap 空间。在调用 malloc 之前,brk的值等于start_brk,即heap大小为0。ptmalloc在开始时,若请求的空间小于 mmap分配阈值(mmap threshold,默认值为 128KB)时,主分配区会调用 sbrk()增加一块大小为 (128 KB + chunk_size) align 4KB 的空间作为 heap。非主分配区会调用 mmap 映射一块大小为HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统上默认为 64MB)的空间作为 sub-heap。当用户请求内存分配时,首先会在这个区域内找一块合适的 chunk 给用户。当用户释放了 heap 中的 chunk 时,ptmalloc 使用 fastbins 和 bins 来组织空闲 chunk。以备用户的下一次分配。若需要分配的 chunk 大小小于 mmap分配阈值,而 heap 空间又不够,则此时主分配区会通过 sbrk()调用来增加 heap 大小,非主分配区会调用 mmap 映射一块新的 sub-heap,增加 top chunk 的大小. (每次 heap 增加的值都会对齐到 4KB
当用户的请求超过 mmap 分配阈值,并且主分配区使用 sbrk()分配失败的时候,或是非 主分配区在 top chunk 中不能分配到需要的内存时,ptmalloc 会尝试使用 mmap()直接映射一块内存到进程内存空间。使用 mmap()直接映射的 chunk 在释放时直接解除映射,而不再属于进程的内存空间。任何对该内存的访问都会产生段错误。当 ptmalloc munmap chunk 时,如果回收的 chunk 空间大小大于 mmap 分配阈值的当前值,并且小于 DEFAULT_MMAP_THRESHOLD_MAX(32 位系统默认为 512KB,64 位系统默认为 32MB),ptmalloc 会把 mmap 分配阈值调整为当前回收的 chunk 的大小,并将 mmap 收缩阈值(mmap trim threshold)设置为 mmap 分配阈值的 2 倍。这就是 ptmalloc 的对 mmap分配阈值的动态调整机制,该机制是默认开启的,当然也可以用 mallopt()关闭该机制.



内存分配算法概述

32位系统下 (64位系统类似32位

  • 小于等于 64 字节:用 pool 算法分配。
  • 64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。
  • 大于等于 512 字节:用最佳匹配算法分配。
  • 大于等于 mmap 分配阈值(默认值 128KB):根据设置的 mmap 的分配策略进行分配,如果没有开启 mmap 分配阈值的动态调整机制,大于等于 128KB 就直接调用mmap分配。否则,大于等于mmap 分配阈值时才直接调用 mmap()分配。

ptmalloc 的响应用户内存分配要求的步骤

  1. 获取分配区的锁,为了防止多个线程同时访问同一个分配区,在进行分配之前需要取得分配区域的锁。线程先查看线程私有实例中是否已经存在一个分配区,如果存在尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,否则,该线程搜索分配区循环链表试图获得一个空闲(没有加锁)的分配区。如果所有的分配区都已经加锁,那么 ptmalloc 会开辟一个新的分配区,把该分配区加入到全局分配区循环链表和线程的私有实例中并加锁,然后使用该分配区进行分配操作。开辟出来的新分配区一定为非主分配区。开辟非主分配区时会调用 mmap()创建一个 sub-heap,并设置好 top chunk。
  2. 将用户的请求大小转换为实际需要分配的 chunk 空间大小。
  3. 判断所需分配chunk的大小是否满足chunk_size <= max_fast (max_fast 默认为 64B),如果是的话,则转下一步,否则跳到第 5 步。
  4. 首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分配结束。否则转到下一步。
  5. 判断所需大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果chunk 大小处在 small bins 中,则转下一步,否则转到第 7 步。
  6. 根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘取一个恰好满足大小的 chunk。若成功,则分配结束,否则,转到下一步。
  7. 到了这一步,说明需要分配的是一块大的内存,或者 small bins 中找不到合适的chunk。于是,ptmalloc 首先会遍历 fast bins 中的 chunk,将相邻的 chunk 进行合并,并链接到 unsorted bin 中,然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只有一个 chunk,并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束,否则将根据 chunk 的空间大小将其放入 small bins 或是 large bins 中,遍历完成后,转入下一步。
  8. 到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。从 large bins 中按照“smallest-first,best-fit”原则,找一个合适的 chunk,从中划分一块所需大小的 chunk,并将剩下的部分链接回到 bins 中。若操作成功,则分配结束,否则转到下一步。
  9. 如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来进行分配。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 top chunk 中分出一块来。否则转到下一步。
  10. 到了这一步,说明 top chunk 也不能满足分配要求,于是就有两个选择: 如果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用 mmap来分配一个新的 sub-heap,增加 top chunk 大小;或者使用 mmap()来直接分配。在这里,需要依靠 chunk 的大小来决定使用哪种方法。判断所需分配的chunk大小是否大于等于 mmap 分配阈值,如果是的话,则转下一步,调用 mmap 分配,否则跳到第 12 步,增加 top chunk 的大小。
  11. 使用 mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。然后将内存指针返回给用户。
  12. 判断是否为第一次调用 malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。若已经初始化过了,主分配区则调用 sbrk()增加 heap 空间,分主分配区则在 top chunk 中切割出一个 chunk,使之满足分配需求,并将内存指针返回给用户。


内存回收算法概述

free() 函数接受一个指向分配区域的指针作为参数,释放该指针所指向的 chunk。而具体的释放方法则依据 chunk 所处的位置和大小决定, 具体步骤:

  1. free()函数首先需要获取分配区的锁,来保证线程安全。
  2. 判断传入的指针是否为 null,如果为 null,则什么都不做,直接 return。否则转下一步。
  3. 判断所需释放的 chunk 是否为 mmaped chunk,如果是,则调用 munmap()释放mmaped chunk,解除内存空间映射。如果开启了 mmap 分配阈值的动态调整机制,并且当前回收的 chunk 大小大于 mmap 分配阈值,将 mmap分配阈值设置为该 chunk 的大小,将 mmap 收缩阈值设定为 mmap 分配阈值的 2倍,释放完成,否则跳到下一步。
  4. 判断 chunk 的大小和所处的位置,若 chunk_size <= max_fast,并且 chunk 并不位于heap 的顶部,即不与 top chunk 相邻,则转到下一步,否则跳到第 6 步。(因为与 top chunk 相邻的小 chunk 优先和 top chunk 进行合并,所以这里不仅需要判断大小,还需要判断相邻情况, 相邻则不会放到fast bins中, 而是被吸收进top chunk)
  5. 将 chunk 放到 fast bins 中,chunk 放入到 fast bins 中时,并不修改该 chunk 使用状态位 P。也不与相邻的 chunk 进行合并。然后程序从free()函数返回.
  6. 判断前一个 chunk 是否处在使用中,如果前一个块也是空闲块,则合并。并转下一步。
  7. 判断当前释放 chunk 的下一个块是否为 top chunk,如果是,则转第 9 步,否则转下一步。
  8. 判断下一个 chunk 是否处在使用中,如果下一个 chunk 也是空闲的,则合并,并将合并后的 chunk 放到 unsorted bin 中。注意,这里在合并的过程中,要更新chunk的大小,以反映合并后的 chunk 的大小。并转到第 10 步。
  9. 如果执行到这一步,说明释放了一个与 top chunk 相邻的 chunk。则无论它有多大,都将它与 top chunk 合并,并更新 top chunk 的大小等信息。转下一步。
  10. 判断合并后的 chunk 的大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD(默认64KB),如果是的话,则会触发进行 fast bins 的合并操作,fast bins 中的 chunk 将被遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中。 fast bins 将变为空,操作完成之后转下一步。
  11. 判断 top chunk 的大小是否大于 mmap 收缩阈值(默认为 128KB),如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的128KB 空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配请求;如果为非主分配区,会进行 sub-heap 收缩,将 top chunk 的一部分返回给操作系统,如果 top chunk 为整个 sub-heap,会把整个 sub-heap 还回给操作系统。做完这一步之后,释放结束,从 free() 函数退出。

可以看出,收缩堆的条件是当前free的chunk大小加上前后能合并chunk的大小大于64k,并且要 top chunk 的大小要达到 mmap 收缩阈值,才有可能收缩堆。



配置选项概述

Ptmalloc 主要提供以下几个配置选项用于调优,这些选项通过mallopt()进行设置:

1. M_MXFAST: 用于设置 fast bins 中保存的 chunk 的最大大小,默认值为 64B, 最大值为 80B,不能设置比 80B 更大的值,因为设置为更大的值并不能提高分配的速度。如果设置该选项为 0,就会不使用 fast bins。

2. M_TRIM_THRESHOLD: 用于设置 mmap 收缩阈值,默认值为 128KB。自动收缩只会在 free时才发生,如果当前 free 的 chunk 大小加上前后能合并 chunk 的大小大于 64KB,并且 top chunk 的大小达到 mmap 收缩阈值,对于主分配区,调用 malloc_trim()返回一部分内存给操作系统,对于非主分配区,调用 heap_trim()返回一部分内存给操作系统,在发生内存收缩时,会重置 mmap 分配阈值和 mmap 收缩阈值。M_TRIM_THRESHOLD 的值必须设置为页大小对齐,设置为-1 会关闭内存收缩设置。

3. M_MMAP_THRESHOLD: 用于设置 mmap 分配阈值,默认值为 128KB,ptmalloc 默认开启动态调整 mmap 分配阈值和 mmap 收缩阈值。当用户需要分配的内存大于mmap分配阈值,ptmalloc的malloc()函数其实相当于mmap()的简单封装,free 函数相当于 munmap()的简单封装。相当于直接通过系统调用分配内存,回收的内存就直接返回给操作系统。

4. M_MMAP_MAX: 用于设置进程中用 mmap 分配的内存块的最大限制,默认值为 64K,因为有些系统用 mmap 分配的内存块太多会导致系统的性能下降。如果将 M_MMAP_MAX 设置为 0,ptmalloc 将不会使用 mmap 分配大块内存。Ptmalloc 为优化锁的竞争开销,做了 PER_THREAD 的优化.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值