glibc大量小内存的释放

  • Post author:
  • Post category:其他


之前在项目中,发现内存在某些情况下飙升,然后无法降下来的情况,记录一下解决过程。

大概情况就是:程序在运行中需要不停申请小块内存用来接收传输过来的视频数据,将数据保存到磁盘后然后释放掉free掉,正常情况下,总的占用内存维持在一个合理的水平,但是如果磁盘出现写入性能下降的情况时,造成接收的数据堆积,此时需要去申请更多的内存来缓存数据,此时进程总的占用内存变显著增大,这种情况也是可接受的。如果接下来磁盘写入性能恢复正常,缓存的数据减少,大部分申请的内存释放,此时进程占用的总内存应该恢复到正常水平。但是问题来了,在项目中发现,如果进程使用内存一旦因为异常情况升高,内存几乎是再也降不下来了。

开始怀疑是程序内存泄漏,在使用各种方法定位后排除了程序本身内存泄漏的可能,而且确认是调用了free释放掉了内存,那free后的内存为何没有归还给操作系统?

经过查资料,得知Linux下内存管理是由glic库来与内核交互,即用户空间是通过glic来进行的系统调用。glic提供两种方式来申请内存,分别是brk和mmap,当通过malloc/new申请的内存小于

M_MMAP_THRESHOLD

(缺省128K)时,glic调用brk来申请内存,当要申请的内存大于M_MMAP_THRESHOLD时,glic调用mmap来申请内存。这两种方式分配的都是虚拟内存,没有分配物理内存。

在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

brk是将数据段(.data)的最高地址指针_edata往高地址推,完成虚拟内存分配;而通过mmap系统调用分配的内存是在堆和栈的中间空闲地址分配一块虚拟内存,这样释放时可以不受约束地自由释放。这样通过brk分配的内存是连续的一块空间,如下图中依次brk申请ABD内存,释放的时候,若高地址的内存不释放,低地址的内存是不能释放的,如下图(7);而mmap申请的内存可以自由释放,如下图(6)。

当通过brk释放的内存相邻的加起来达到

M_TRIM_THRESHOLD

(缺省128K)时,会进行内存紧缩,如下图,先释放B,B的内存并没有真正释放,再释放D时,B+D>128K,此时这一块内存组就会释放掉。

那我们是不是可以通过设置

M_MMAP_THRESHOLD



M_TRIM_THRESHOLD

这两个内存相关属性的值来控制内存的分配和释放呢,答案是可以的,glic提供了mallopt函数来设置这些属性,一个专门调节相关阈值的函数。


int mallopt (int param, int value);

通过设置M_MMAP_THRESHOLD的值,来控制何时调用mmap,针对我遇到的问题,我想到的是减小M_MMAP_THRESHOLD的值,这样大部分时候申请的内存都是用mmap申请的,每次调用delete/free释放后内存也会调用munmap及时归还内存给操作系统。既然理论上可行,那就测试一下效果,将M_MMAP_THRESHOLD设置的比较小(10K)进行测试,发现内存确实是归还操作系统,程序占用的内存一直维持较低水平,但是cpu负载变高,反过来还影响了正常的业务。这里是因为,每次的mmap操作都会产生缺页中断,每次都要内核重新分配释放内存,增加性能负担。

注:

从这篇文章

了解到,Glibc的新特性:

M_MMAP_THRESHOLD可以动态调整

。M_MMAP_THRESHOLD的值在128KB到32MB(32位机)或者64MB(64位机)之间动态调整,每次申请并释放一个大小为2MB的内存后,M_MMAP_THRESHOLD的值被调整为2M到2M + 4K之间的一个值(具体可以参考

Glibc的patch说明

)。当显示调用mallopt设置

M_MMAP_THRESHOLD

的值后,该动态调整特性失效。

glic调用brk实际可以看作是一个内存的内存池,大量小块的内存,在free释放时先不归还操作系统,等到下次再用时,再从这部分里面取,这样避免了频繁的缺页中断,提升性能。

既然改小M_MMAP_THRESHOLD的值带来的是性能的下降,那试一下设置M_TRIM_THRESHOLD的值,通过调小M_TRIM_THRESHOLD的值来让内存紧缩的更频繁,这样是否可行呢,答案是效果不好,几乎没有作用,因为对于brk申请的内存空间,只要高地址的内存不释放,下面的内存无论free释放多少,都不会真正归还操作系统。可以通过下面代码验证:

int main()
{
    mallopt(M_TRIM_THRESHOLD, 1024);
    printf("finished M_TRIM_THRESHOLD\n");     
    mallopt(M_TOP_PAD, 0);

    char *p[11];     
    int i;     
    /* 开辟 11 片内存 */
    for(i = 0; i < 11; i++)     
    {         
        p[i]=(char*)malloc(1024*2);         
        strcpy(p[i], "123");     
    }     
     /* 只释放10片内存 */
    for(i = 0; i < 10; i++) 
    {         
        free(p[i]);     
    }   

    pid_t pid=getpid();     
    printf("pid = %d\n", pid);  
    pause(); 

    return 0; 
}


开始使用该函数,通过设置参数

M_TRIM_THRESHOLD的值,来让glic自动释放内存,反复调整参数2的值然后测试,效果都不理想,要么是释放的内存有限,要么就是内存虽然及时得到了释放,但是却极大地降低了服务器性能,反而影响了正常的业务。


这里要分享一篇文章

,这篇文章作者的目的是降低系统态的cpu占用,最后作者通过下面的方法来达成:(上面的内存分布图来源于该文章)

禁止malloc调用mmap分配内存,禁止内存紧缩。

在进程启动时候,加入以下两行代码:

mallopt(M_MMAP_MAX, 0);         // 禁止malloc调用mmap分配内存

mallopt(M_TRIM_THRESHOLD, -1);  // 禁止内存紧缩

但这种方法在不适合解决我的问题,因为我的目标是:程序在极端情况时内存会升很高(比正常情况高几倍),但是这种极端情况持续时间不会太长,随着环境的正常,那程序占用的内存不能一直高居不下,否者内存会是极大的浪费,所以要想办法去释放那部分极端情况下多分配但后面又长时间空闲的内存。

好在,glic还提供了下面这个函数:


void malloc_trim(size_t pad);

malloc_trim(0)函数尝试在堆的顶部释放可用内存,按照man手册的说法,只能释放堆顶部的内存,空洞无法释放,但是经过下面代码测试确实可以释放的,即便该空闲内存顶部有仍在使用的内存或者该内存块未达到M_TRIM_THRESHOLD大小,调用malloc_trim(0)后这些内存空洞仍然会归还操作系统。

#include <malloc.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
		mallopt(M_TRIM_THRESHOLD, 1024);
		printf("finished M_TRIM_THRESHOLD\n");     
		mallopt(M_TOP_PAD, 0);

		char *p[11];     
		int i;     
		/* 开辟 11 片内存 */
		for(i = 0; i < 11; i++)     
		{         
				p[i]=(char*)malloc(1024*2);         
				strcpy(p[i], "123");     
		}     
		/* 只释放10片内存 */
		for(i = 0; i < 10; i++) 
		{         
				free(p[i]);     
		}   

		pid_t pid=getpid();     
		printf("pid = %d\n", pid);  
		
        /*释放堆顶下面的空洞*/
		malloc_trim(0);

		pause(); 

		return 0; 
}

经过查看

smaps

属性,cat /proc/pid/smaps 可以发现内存已经释放。

在项目中测试使用

malloc_trim(0)

函数,释放内存非常及时,并且对性能的影响很小。但是即便是没影响业务,一次释放大量内存产出的系统调用还是会降低性能的,避免调用太过频繁,可以通过定时器获取进程的内存占用,来决定是否触发malloc_trim(0)调用,或者使用定时器周期性地执行,间隔时间不宜过短。

总结:linux下内存管理由glic库完成,小内存由brk系统调用申请,该部分的内存实现相当于是个内存池,申请、释放等由glic库管理。大部分时间我们不必考虑调用mallop()、malloc_trim()等函数,需要结合具体的业务场景来选择合适的方案。

参考:


https://bbs.csdn.net/topics/330179712


https://www.cnblogs.com/lookof/archive/2013/03/26/2981768.html


https://www.jianshu.com/p/bc61df40d85d


https://stackoverflow.com/questions/10943907/linux-allocator-does-not-release-small-chunks-of-memory



版权声明:本文为u013259321原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。