内存分配器
开源社区现有内存分配器(Memory Allocator)的种类:
- 第一个被广泛使用的通用动态内存分配器,dlmalloc
- glibc内置内存分配器的原型,ptmalloc
- FreeBSD & Firefox使用的分配器,jemalloc
- Google贡献的分配器,tcmalloc
- Solaris所用的分配器
- 其他内存分配器
glibc内存分配-ptmalloc
ptmalloc的介绍
ptmalloc的前身是dlmalloc,在dlmalloc的基础上,它支持了多线程内存分配。
在dlmalloc中,所有线程都公用同一个空闲内存管理列表,在同一个时段,只能有一个线程访问临界区,其他线程阻塞在malloc上,导致性能低下;
ptmalloc提供了多线程支持,每个线程都维护单独的堆(包含空闲内存管理列表),因此各个线程可以并发的从空闲内存列表中申请内存。在主线程中维护的连续堆区称为main arena,在子线程中维护的堆区称thread arena;
- main arena和thread arena不同之处:
1.ptmalloc中主线程的main arena使用brk来建造或扩容,而子线程中的thread arena使用mmap来建造或扩容。
2.main arena只会维护一个堆,当当前维护的堆空间不足时,通过brk来扩充;而thread arena可能维护多个堆,空间不足时通过mmap申请另一段和当前不连续堆结构的内存(这些堆结构内存共享同一个malloc_state,由它统一管理)。
特别注意的是arena的数量不是与线程的数量对应的,每个线程都拥有一个arena消耗太大,一般arena的数量为cpu核心数的整数倍(32位系统2倍,64位系统8倍)
若是不更改堆的配置阙值,一般低于132k的内存使用堆来分配,高于等于132k的内存使用mmap来分配
ptmalloc中堆区基本结构
- heap_Info
堆的头部元数据信息,一个thread arena需要维护多个堆,每个堆都有自己的堆头,heap_info即堆头信息;而main arena无需维护多个堆,因此无需此结构。
为什么thread arena需要维护多个堆?每个 thread arena 开始都只有一个堆,但当这个堆耗尽空间时,新mmap的堆并不能连续开辟到这个 arena 里,因此需要建立多个堆。 - malloc_chunk
chunk的基本结构,根据用户的请求,每个堆被分为若干个chunk,每个chunk都有保存自己结构信息的malloc_chunk; - malloc_state
主堆区(main arena)和各线程堆区(thread arena)的状态信息,例如对bins、top chunk、last remainder chunk等结构信息的描述;
main arena 的malloc_state是作为一个全局变量保存在.data区;
而对于thread arena来说,它的多个堆共享同一个 malloc_state,并且这个malloc_state直接保存在堆中(如下图,右图为main arena,malloc_state单独保存,左图为thread arena,malloc_state保存在堆中)。
查看测试内存布局可浏览:http://www.vuln.cn/6975
内存的管理最小数据结构chunk
chunk是管理堆中内存的最小数据结构,一个堆会被分隔成多个不同种类的chunk,不同种类的chunk通过链表的形式组织在一起又被称为Bin。
/*
* chunk结构体:
* mchunk_prev_size:表示前一个chunk的大小,若前一个chunk没有被释放,则无效;
* mchunk_size:当前chunk的大小,并通过位域记录了当前chunk和上一个chunk的相关属性:前一个chunk
* 是否在使用中(P)、当前chunk是否是mmap分配(M)、当前chunk是否属于非主分配区(A);
* fd/bk:只有当当前chunk是空闲时才使用,将当前chunk加入到空闲chunk链表中同一管理,
* 若当前chunk正在使用,则该空间用来保存用户数据;
* fd_nextsize/bk_nextsize:只有当当前chunk时空闲时才使用(使用中同上),large bin中的chunk是按
* 大小排列的,fd_nextsize指向比当前chunk大的第一个链表节点,bk_nextsize指定比其小的第一个节点;
*/
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_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;
};
-
一个正在使用中的chunk的描述:
-
一个空闲的chunk的描述:
注:上述的nextchunk是逻辑地址中线性的next地址,而非链表中的next地址 -
Chunk的种类
- Allocated chunk(正在使用中的chunk)
- Free chunk(空闲的chunk)
- Top chunk
当所有的bin都无法响应用户的内存分配请求时,就会使用Top chunk来响应用户请求,当用户请求的数据比top chunk大时,top chunk使用brk扩容;请求数据比top chunk小时,在top chunk中分配适合内存返回给用户,剩余部分继续用作top chunk; - Mmaped chunk
当请求的空间超过了分配阙值后就会使用mmap来分配内存; - Last remainder chunk
和top chunk及mmaped chunk一样,不会在任何bin中找到这种chunk,当需要分配一个small chunk,但在small bin中找不到合适的chunk时,这是如果Last remainder chunk大小大于需要的chunk,即会从它中分割相应的chunk返回,剩余部分仍作为Last remainder chunk;
Bin
空闲内存列表结构,统一管理free chunk。当用户使用free函数释放内存后,ptmalloc不会把内存马上归还给操作系统,而是将它交给ptmalloc本身的空闲链表bin管理起来,这样后续需要malloc一块内存时,会首先考虑从bin中寻找一块大小合适的chunk返回给用户,避免频繁的系统调用。
ptmalloc将大小相近的chunk用链表串连起来,这样的一个链表就被称为bin。ptmalloc一共维护了128个bin,每个bin都维护了大小相近的双向链表chunk。基于chunk的大小,分为以下几类bin:
- fast bin
- unsorted bin(缓冲)
- samll bin
- large bin
bins采用数组(数组的成员保存链表头,哈希表拉链)的方式来保存:
fastbinsY数组保存fast bin ,其他bin用bins数组保存;
当用户调用malloc时,能很快找到用户需要分配的内存大小是否在维护的bins上,如果在某个bin上,就可以通过双向链表去查找合适的chunk内存块给用户使用。
不同种类bin的介绍
- fast bin
程序在运行时会经常需要申请和释放一些较小的内存空间。当分配器合并了相邻的几个小的 chunk 之后,也许马上就会有另一个小块内存的请求,这样分配器又需要从大的空闲内存中切分出一块,这样无疑是比较低效的,故而,malloc 中在分配过程中引入了 fast bin;
fast bin是bin的高速缓冲区,大约有10个定长队列。每个fast bin都记录着一条free chunk的单链表(称为binlist ,采用单链表是出于fast bin中链表中部的chunk不会被摘除的特点),增删chunk都发生在链表的前端。 fast bin 记录着大小以8字节递增的bin链表。
当用户释放一块不大于max_fast(默认值64B)的chunk的时候,会默认会被放到fast bin上。当需要给用户分配的 chunk 小于或等于 max_fast 时,malloc 首先会到fast bin上寻找是否有合适的chunk;
除非特定情况,两个毗连的空闲chunk并不会被合并成一个空闲chunk。不合并可能会导致碎片化问题,但是却可以大大加速释放的过程!
分配时,binlist中被检索的第一个个chunk将被摘除并返回给用户。free掉的chunk将被添加在索引到的binlist的前端。 - unsorted bin(缓冲)
unsorted bin 的队列使用 bins 数组的第一个,是bins的一个缓冲区,加快分配的速度。当用户释放的内存大于max_fast或者fast bin合并后的chunk都会首先进入unsorted bin上。chunk大小 无尺寸限制,任何大小chunk都可以添加进这里。这种途径给予 ‘glibc malloc’ 第二次机会以重新使用最近free掉的chunk,这样寻找合适bin的时间开销就被抹掉了,因此内存的分配和释放会更快一些。
用户malloc时,如果在 fast bin 和 samll bin中没有找到合适的 chunk,则malloc 会在 unsorted bin 中查找合适的空闲 chunk,如果没有合适的bin,ptmalloc会将unsorted bin上的chunk放入bins上,然后到bins上查找合适的空闲chunk。 - samll bin
大小小于512字节的chunk被称为small chunk,而保存small chunks的bin被称为small bin。数组从2开始编号,前64个bin为small bin,small bin每个bin之间相差8个字节,同一个small bin中的chunk具有相同大小。
每个small bin都包括一个空闲区块的双向循环链表(也称binlist)。free掉的chunk添加在链表的前端,而所需chunk则从链表后端摘除。
两个毗连的空闲chunk会被合并成一个空闲chunk。合并消除了碎片化的影响但是减慢了free的速度。
分配时,当samll bin非空后,相应的bin会摘除binlist中最后一个chunk并返回给用户。在free一个chunk的时候,检查其前或其后的chunk是否空闲,若是则合并,也即把它们从所属的链表中摘除并合并成一个新的chunk,新chunk会添加在unsorted bin链表的前端。 - large bin
大小大于等于512字节的chunk被称为large chunk,而保存large chunks的bin被称为large bin,位于small bin后面。large bin中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小递减排序,大小相同则按照最近使用时间排列。
两个毗连的空闲chunk会被合并成一个空闲chunk。
分配时,遵循原则“smallest-first , best-fit”,从顶部遍历到底部以找到一个大小最接近用户需求的chunk。一旦找到,相应chunk就会分成两块User chunk(用户请求大小)返回给用户。Remainder chunk剩余大小添加到unsorted bin。free时和small bin 类似。
malloc和free的过程
malloc
tip:每次内存分配时,都是分配的一个chunk(完整的chunk或是从chunk分割小的chunk),当一个chunk被分配后逻辑上即可脱离bins分配给用户
内存分配时,检索bin的优先级:
- fastbin
- small bin
- unsorted bin
- large bin
- top chunk
具体的分配过程(简要描述,实际分配过程还有多个循环):
1.获取分配区锁
2.判断请求内存大小是否满足size <= max_fast(默认64B),如果是,跳到下一步,否则,跳到第四步
3.在fast bins寻找并返回一个合适大小的chunk,结束
4.判断所需要的大小是否属于small部分(即判断请求大小< 150 B),如果是,跳到下一步,否则,跳到第六步
5.根据具体的size在small bin中找到合适的chunk返回,结束,否则下一步
6.到了这一步,说明需要的内存是一块大内存,或者small bin中找不到合适的大小;首先遍历fast bin,将相邻的chunk合并,并链接到unsorted bin中,然后遍历unsorted bin,“如果unsorted bin只有一个chunk,并且这个chunk是remainder chunk(在上次分配时被切割过),并且所需分配的chunk大小属于small bins,并且chunk的大小大于等于需要分配的大小”,这种情况下就直接将该chunk进行切割,分配结束;否则将根据chunk的空间大小将其放入small bins或是large bins中,遍历完成后,转入下一步
7.到了这一步,说明samll bin和unsorted bin都没找到合适的chunk,并且fastbin和unsorted bin中的所有chunk都清除干净;在large bin中按照smallest-first,best-fit的原则找到合适的chunk切割分配返回,否则下一步
8.top chunk分配,若top chunk不满足则下一步
9.到了这一步,说明top chunk也不满足。所以,有了两种选择,如果是在主分配区,则调用brk增加top chunk的大小;如果是非主分配区,调用mmap来分配一个新的sub-heap增加top chunk的大小
简单理解malloc(不考虑mmap和第一次使用malloc):
1.ptmalloc首先会查找fast bin
2.如果不能找到合适的chunk,则查找small bin
3.如果不能找到合适的chunk,合并fast bin,将合并后的chunk放到unsorted bin中,查找unsorted bin
4.如果不能找到合适的chunk,将unsorted bin中的chunk放到bins(small、large)中,查找large bin
5.如果不能找到合适的chunk,则考虑top chunk及系统调用
free
具体的释放过程:
1.获取分配区锁
2.判断传入指针是否为零,是,直接返回
3.判断需要释放的chunk是否属于mmap申请,若是则直接解除内存映射
4.判断需要释放的chunk的大小chunk <max_fast,若是则跳到下一步,否则跳到第六步(这里需要注意,若是需要释放的chunk与top chunk相邻,若是也跳到第六步)
5.将chunk放到fast bin中,结束
6.判断前一个chunk是否使用中,若没有使用,合并,跳到下一步
7.判断当前chunk下一个chunk是否是top chunk,若是,跳到第九步,否则,下一步
8.判断当前chunk下一chunk是否使用,若未使用,则合并,将合并后的chunk放入bins中(unsorted bin),并跳到第十步
9.将chunk与top chunk合并,跳到下一步
10.判断合并后的chunk大小是否大于FASTBIN_CONSOLIDATION_THRESHOLD,如果是,则会触发fast_bin合并操作,遍历fast_bin,将相邻的空闲chunk合并,并放到bins(unsorted bin)中,跳到下一步
11.判断 top chunk 的大小是否大于 DEFAULT_TRIM_THERESHOLD,如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统.,但是最先分配的128KB的空间是不会归还,ptmalloc 会一直控制这部分内存,用于响应用户的分配请求;如果是非主分配区,会进行heap-sub收缩,将top chunk部分归还给操作系统,做完这一步之后,释放结束,从 free 函数退出.
简单理解free(不考虑mmap):
1、如果在fastbin范围内就优先释放到fastbin
2、否则就尝试前后合并:
a、合并后的chunk靠近top chunk,那就并到top chunk;
b、合并后的chunk不靠近top chunk,那就放到unsorted bin;
c、判断是否触发清空fast bin(合并fast bin放到unsorted bin中);
d、判断是否回收给操作系统
注意:free的过程并不和small bin、large bin打交道,只是当malloc的时候,进入到malloc的大循环中处理unsorted bin的时候才会把unsorted bin里面的块按照大小放到smal、large bin里面
参考:Glibc内存管理华庭
想要研究ptmalloc源码的这里推荐:https://bbs.pediy.com/thread-258747.htm
ptmalloc内存检测
系统库函数提供了函数打印当前进程的内存使用状况:
malloc_stats(),执行结果如下:
Arena 0://第一个arena(每个线程分配一个arena),这里只有一个线程
system bytes = 135168//本线程从操作系统获得的动态内存,这里是132KB
in use bytes = 1152//本线程在使用的动态内存,1152字节
Total (incl. mmap)😕/总的使用情况,各个线程使用动态内存的累加值
system bytes = 135168//本进程从操作系统获得的动态内存,这里是132KB
in use bytes = 1152//本进程在使用的动态内存,1152字节
max mmap regions = 0//当一次申请内存超过128KB(32位操作系统)或1MB(64位操作系统)时,会增加mmap区域,这里统计使用mmap区域的个数
max mmap bytes = 0//mmap区域对应内存大小
malloc_info(opt, fd),以xml的形式输出当前进程的内存分配情况,包括各个线程的内存使用情况;
mallinfo(),目前已经不再更新(使用的int型数据,64位系统可能溢出),可以打印处相关bin的数量(可以根据bin的数量来判断内存碎片):
struct mallinfo {
int arena; /* non-mmapped space allocated from system /
int ordblks; / number of free chunks /
int smblks; / number of fastbin blocks /
int hblks; / number of mmapped regions /
int hblkhd; / space in mmapped regions /
int usmblks; / maximum total allocated space /
int fsmblks; / space available in freed fastbin blocks /
int uordblks; / total allocated space /
int fordblks; / total free space /
int keepcost; / top-most, releasable (via malloc_trim) space */
};
动态内存和物理内存之间的关系
当使用malloc或new分配内存后(分配内存的过程中使得堆区上顶了),若是没有立即写数据到分配的内存中,此时所分配的内存仍然是虚拟内存,只有在写入数据后才会和相应的物理内存对应。
物理内存和虚拟内存的理解:https://www.cnblogs.com/panchanggui/p/9288389.html
- 每个进程的4G内存空间只是虚拟内存空间,每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址
- 所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。
- 进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,需要用页表来记录
- 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
- 当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则缺页异常
- 缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘
页的理解:为了方便内存命中,一般把一段连续数据称为一页(通常一页为4096Byte);例如,我们可以将4G的虚拟内存分给成很多4k页,然后把物理内存看作很多连续的4k页框,这样虚拟内存到物理内存的映射就是将页放入到页框中(帮助理解,实际处理很复杂)。
linux使用tcmalloc替换ptmalloc
1.首先下载gperftools,编译(configure开启libtcmalloc_minimal选项),编译需要依赖libunwind和liblzma这两个库。
2.编译自己的项目,在编译时加入-ltcmalloc_minimal参数。
3.运行,可以使用gdb调试查看此时使用的库是不是tcmalloc(在某个分配函数malloc或者new处设置断点,运行程序后使用s跟进,大概会看到以下信息)
Breakpoint 2, 0x00007ffff7ba8c20 in tc_malloc () from /lib64/libtcmalloc_minimal.so.4
(gdb) bt
#0 0x00007ffff7ba8c20 in tc_malloc () from /lib64/libtcmalloc_minimal.so.4
#1 0x00007ffff729e45d in __fopen_internal () from /lib64/libc.so.6
有关tcmalloc的内存强制回收函数:MallocExtension::instance()->ReleaseFreeMemory(),此时使用malloc_trim时无效的;
MallocExtension::instance()->GetStats(buf, 2048)和malloc_stats()都可以获取当前状态;
通过/proc/pid/status来查看内存使用情况
VmPeak 进程所使用的虚拟内存的峰值
VmSize 进程当前使用的虚拟内存的大小
VmLck 已经锁住的物理内存的大小(锁住的物理内存不能交换到硬盘)
VmHWM 进程所使用的物理内存的峰值
VmRSS 进程当前使用的物理内存的大小
VmData 进程占用的数据段大小
VmStk 进程占用的栈大小
VmExe 进程占用的代码段大小(不包括库)
VmLib 进程所加载的动态库所占用的内存大小(可能与其它进程共享)
VmPTE 进程占用的页表大小(交换表项数量)
VmSwap 进程所使用的交换区的大小
记录其他一些关于内存问题
madvise函数
当我们使用malloc或是mmap分配内存时,linux下采用的是延时分配,只有当真正使用这块内存时才会分配物理内存地址,在真正使用前都是采用分配虚拟地址的模式;由此将会导致在使用这块内存时将会产生缺页中断,当这块内存很大时,相应的缺页中断的次数也会更多;一个好的方法是通过madvise函数提前告知操作系统将这块内存直接载入到物理内存中;
具体的函数说明可以看man page;
其参数三指定为MADV_DONTNEED时,操作系统会将进程指定范围内的内容从物理内存上回收(下次使用时需要reload);
在glibc的malloc_trim函数中会做此操作,malloc_trim的步骤是,先将释放的内存使用madvise回收物理内存,然后将堆顶可以回收的内存使用sbrk返回给操作系统;(注意MADV_DONTNEED只是从物理内存回收了,并没有返回给操作系统,此时RSS的值应该减少了,该区域内存还是可以通过缺页中断使用的)
swap交换分区
Swap分区在系统的物理内存不够用的时候,把硬盘内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。
名词
RSS( Resident Set Size )常驻内存集合大小,表示相应进程在RAM中占用了多少内存,并不包含在SWAP中占用的虚拟内存。即使是在内存中的使用了共享库的内存大小也一并计算在内,包含了完整的在stack和heap中的内存。
VSZ (Virtual Memory Size),表明是虚拟内存大小,表明了该进程可以访问的所有内存,包括被交换的内存和共享库内存。