内存暴涨问题细探
1. 进程虚拟空间
进程虚拟空间如下图:
如上图示:最高的1G空间保留给内核使用。接下来是栈,栈向低地址方向延伸(栈的大小受RLIMIT_STACK限制,默认为8M),下面是MMAP区(文件映射内存,如动态库等,SPP微线程的私有栈也位于这里),下面是堆(动态内存增长),堆向高地址方向延伸,接下来依次是BSS、数据段、代码段。
2. Linux下动态内存分配实现机制
C、C++的动态内存分配、管理都是基于malloc和free的,动态内存即虚拟空间堆区。另外多说一句,malloc和free操作的也是虚拟地址空间。
malloc,动态内存分配函数。是通过brk(sbrk)和mmap这两个系统调用实现的。
结合上文进程虚拟空间图,brk(sbrk)是将数据段(.data)的最高地址指针_edata往高地址推。mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种实现方式的区别大致如下:
brk(sbrk),性能损耗少; mmap相对而言,性能损耗大
mmap不存在内存碎片(是物理页对齐的,整页映射和释放); brk(sbrk)可能存在内存碎片(由于new和delete的顺序不同,可能存在空洞,又称为碎片)
无论是通过brk(sbrk)还是mmap调用分配的内存都是虚拟空间的内存,只有在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
delete,动态内存释放函数。如果是brk(sbrk)分配的内存,直接调用brk(sbrk)并传入负数,即可缩小Heap区的大小;如果是mmap分配的内存,调用munmap归还内存。无论这两种那种处理方式,都会立即缩减进程虚拟地址空间,并归还未使用的物理内存给操作系统。
brk(sbrk)和mmap都是系统调用,如果程序中频繁的进行内存的扩张和收缩,每次都直接调用,当然可以实现内存精确管理的目的,但是随之而来的性能损耗也很显著。目前大多数运行库(glibc)等都对内存管理做了一层封装,避免每次直接调用系统调用影响性能。如此,就涉及到运行库的内存分配的算法问题了。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
3.缺页中断
如何查看进程发生缺页中断的次数?
用ps -o majflt,minflt -C program命令查看。
majflt代表major fault,中文名叫大错误,minflt代表minor fault,中文名叫小错误。这两个数值表示一个进程自启动以来所发生的缺页中断的次数。
发成缺页中断后,执行了那些操作?
当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:
1、检查要访问的虚拟地址是否合法
2、查找/分配一个物理页
3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)
4、建立映射关系(虚拟地址到物理地址)
重新执行发生缺页中断的那条指令
如果第3步,需要读取磁盘,那么这次缺页中断就是majflt,否则就是minflt。
查看物理内存页使用情况:cat /proc/$PID/smaps,里面详细记录了该进程使用的物理页内存情况,如Private_Dirty、Private_Clean等
mmap系统调用:读写MMAP映射区,相当于读写被映射的文件。本意是将文件当作内存一样读写。相比Read、Write,减少了内存拷贝(Read、Write一个硬盘文件,需要先将数据从内核缓冲区拷贝到应用缓冲区(read),然后再将数据从应用缓冲区拷贝回内核缓冲区(write)。mmap直接将数据从内核缓冲区映拷贝到另一个内核缓冲区),但是被修改的数据从MMAP区同步到磁盘文件上,依赖于系统的页管理算法,默认会慢条斯理得将内容写到磁盘上。另外提供了msync强制同步到磁盘上。
4.Glibc内存分配算法
glibc的内存分配算法,是基于dlmalloc实现的ptmalloc,dlmalloc详细可以参考A Memory Allocator或者Glibc内存分配器。这里主要讲下和内存归还策略相关的,其他内容不做过多扩展。
整体来说,glibc采用的是dlmalloc。为了避免频繁调用系统调用,它内部维护了一个内存池,方便reuse,又称为free-list或bins,如下图示
所有调用delete释放的内存,并不是立即调用brk(sbrk)归还给操作系统,而是先将这个内存块挂在free-list(bins)里面,然后进行内存归并(可选操作,相邻的可用内存块合并为更大的可用内存块),并检查是否达到malloc_trim的threshhold,如果达到了,则调用malloc_trim归还部分可用内存给操作系统。
glibc中,设置了默认进行malloc_trim的threshhold为128K,也就是说当dlmalloc管理的内存池中最大可用内存>128K时,就会执行malloc_trim操作,归还部分内存给操作系统;而在可用内存<=128K时,及时程序中delete了这部分内存,这些内存也是不会归还给操作系统的。表现为:调用delete之后,进程占用的内存并没有减少。
另外,部分glibc的默认设置如下:
DEFAULT_MXFAST 64 (for 32bit), 128 (for 64bit) // free-list(fastbin)最大内存块 DEFAULT_TRIM_THRESHOLD 128 * 1024 // malloc_trim的门槛值 128k DEFAULT_TOP_PAD 0 DEFAULT_MMAP_THRESHOLD 128 * 1024 // 使用mmap分配内存的门槛值 128k DEFAULT_MMAP_MAX 65536 // mmap的最大数量
这些参数都可以通过mallopt进行调整。
malloc_trim(0)可以立即执行trim操作,将内存还给操作系统。
具体fastbin相关的内容,此处不做介绍,前期有很多基于fastbin的堆溢出攻击,感兴趣的同学可以google关键字fastbin搜索下。
5.考虑其他开源库的解决方案
glibc大内存是128k,可以使用tcmalloc或者jemalloc来进行内存管理
tcmalloc是Google开源的一个内存管理库
小对象(<=32K),大对象4k
jemalloc是facebook推出的
Small: [8], [16, 32, 48, …, 128], [192, 256, 320, …, 512], [768, 1024, 1280, …, 3840]
Large: [4 KiB, 8 KiB, 12 KiB, …, 4072 KiB]
Huge: [4 MiB, 8 MiB, 12 MiB, …]
6.参考资料
- A Memory Allocator(dlmalloc, glibc)
- Free/Delete Not Returning Memory To OS?
- Does calling free or delete ever release memory back to the “system”
- How is malloc() implemented internally? [duplicate]
- How do malloc() and free() work?
- 浅析Linux堆溢出之fastbin
- Unix环境高级编程
- 内存分配的原理__进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)