内存暴涨问题细探

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实现的ptmallocdlmalloc详细可以参考A Memory Allocator或者Glibc内存分配器。这里主要讲下和内存归还策略相关的,其他内容不做过多扩展。

整体来说,glibc采用的是dlmalloc。为了避免频繁调用系统调用,它内部维护了一个内存池,方便reuse,又称为free-listbins,如下图示

 

 

所有调用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.参考资料

摘自:一次"内存泄漏"引发的血案

 内存泄漏之malloc_trim

 

 

posted @ 2023-08-17 17:28  PKICA  阅读(112)  评论(0编辑  收藏  举报