记录一次linux应用内存调试过程(续)
创始人
2024-05-31 13:02:51
0

写在前面

本文所描述的内存调试过程,主要是记录最近项目里面遇到的一个内存使用问题。过程大概是,测试软件稳定性时,发现系统内存随着时间的变化,会不断的增长,并且不会恢复。由于怀疑是,应用程序出现了内存泄漏,所以开启了针对于内存泄漏的分析、调试,过程中使用了程序功能模块隔离法、valgrind工具、编写单独程序测试(怀疑是mosquitto存在问题)等方法,最后发现没有内存泄漏的地方。后来,实在找不到问题的原因,甚至怀疑到了glibc内存分配管理器ptmalloc身上,并且将其替换成了jemalloc(Facebook使用的内存分配管理器),换完之后,系统内存使用情况,竟然恢复了正常,当时挺惊喜的。事后想想,还是太年起了,glibc的内存分配器,也是神级人物实现的,性能不可能做的这么差,所以某些事如果觉得奇怪的话,那就不能过早的下结论,需要冷静的思考,借用史强的话说,事出反常必有妖,哈哈!

事情的最后,终于找到了问题的原因,不是应用程序的问题,也不是glibc的问题,原因是应用程序在编译的时候开启了Asan内存检测功能,Asan就是那个妖,哈哈!其实这完全怪自己,当时出于好奇,启用了Asan功能,事后忘了关闭这个功能了,也算是自己挖坑,自己跳了。

Asan可以实时检测程序内部的内存使用情况,一旦发现内存被非法使用就会立即报警,我总结了Asan的基本的原理和常见内存问题的测试程序可以参考下。

使用Asan会占用大量的内存来存储应用的内存使用记录,如果应用不断的进行分配、释放内存的话,存储记录所耗费的内存空间就会不断的增长,这也就对应了前文所说的内存不断增长的问题。

谁偷吃了内存

通过注册hook的方式,跟踪内存分配和释放的热点,具体步骤如下:
1).实现malloc和free包装函数

//编译方式:gcc mymalloc.c -fPIC -shared -o libmymalloc.so
//必须在开头定义该宏
#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
//printf函数里调用了malloc和free,会导致malloc和free的递归调用
//最终导致coredump,这里通过两个标志控制,不会引起递归
static int enable_free_hook   = 1;
static int enable_malloc_hook = 1;
static pid_t gettid() 
{return syscall(SYS_gettid);
}
static void *(*real_malloc)(size_t) = NULL;
static void (*real_free)(void*) = NULL;
void *malloc(size_t size) 
{void *p; void* buffer[128];int line;if (!real_malloc) {real_malloc = dlsym(RTLD_NEXT, "malloc");if (!real_malloc) return NULL;}   p = real_malloc(size);if (enable_malloc_hook) {enable_malloc_hook = 0;//line = backtrace(buffer, 128);//backtrace_symbols_fd(buffer, line, 0);printf("[tpid:%d] malloc(%lu)=%p\n", gettid(), size, p); enable_malloc_hook = 1;}   return p;
}
void free(void *p)
{if (!real_free) {real_free = dlsym(RTLD_NEXT, "free");if (!real_free) return;}if (enable_free_hook) {enable_free_hook = 0;printf("[tpid:%d] free %p\n", gettid(), p);enable_free_hook = 1;}real_free(p);
}  

2).编译成动态库
gcc mymalloc.c -fPIC -shared -o libmymalloc.so -ldl

3).编写测试程序

#include                                                      
#include 
#include 
void test(void)
{char *ptr = malloc(10);if (!ptr) {printf("malloc failed.\n");return;}   memset(ptr, 0, 10);printf("malloc succ, ptr:%p.\n", ptr);free(ptr);
}
int main(void)
{test();return 0;
}

4).测试libmymalloc.so功能

LD_PRELOAD=./libmymalloc.so  ./malloc
[tpid:10131] malloc(10)=0x1cd3010
malloc succ, ptr:0x1cd3010.
[tpid:10131] free 0x1cd3010

解决方案:

基本原理

Linux系统内存管理分为三层:应用层、内存分配器层、内核层。

应用层主要是APP管理本进程里堆栈内存的申请和使用,常见的问题有内存泄漏、内存越界等,出现问题的时候可以使用Asan和valgrind进行探测。内核层实现虚拟内存和物理内存的管理,一般不会有问题,考虑其实现的复杂性,即便是出了问题,也不是一般人可以解决的,哈哈。中间的内存分配器,就是连接应用层和内核层的纽带,类似于内存的批发、零售商,常见内存分配器是:ptmalloc(glibc标配)、tcmalloc(google公司开发)、jemalloc(FreeBSD标配,Facebook维护使用的较多)。

系统默认使用的是ptmalloc,本系统GLIBC的版本是2.21,通过分析学习ptmalloc的实现原理,可以参考这篇文章深入学习下。

ptmalloc的基本原理如下:

  1. 小块儿内存(默认是小于128KB)的内存通过sbrk分配,内存释放后不会立即返还给OS,而是缓存起来,目的是提升小内存的分配效率。
  2. 大块儿内存(默认是大于128KB)的内存通过map分配,通过unmap释放,内存释放后,立即返还给OS,内存的分配和释放性能低。
  3. sbrk分配的内存会采用批发零售的方式,每次申请较大内存块,然后分多次分给应用使用。
  4. 内存紧缩策略会定时将部分内存释放,返还给OS。

ptmalloc的几个缺点:

  1. 内存回收策略导致内存不能真正释放,并返还给OS,比如,ptmalloc内存紧缩策略,如果通过sbrk分配的大块儿内存,多次分给了不同的部分,只有当后分配的内存释放之后,之前分配的内存块才能释放,这样会造成很多本来没有用的内存块不能返还给OS。

  2. 内存管理策略容易导致内存碎片化。

  3. 对于多核、多线程支持不友好。

  4. 内存malloc和free效率不高。

相比之下,jemalloc和tcmalloc对于ptmalloc这些问题进行了改进,降低了内存碎片化,提高了多核、多线程下内存管理性能,其中tcmalloc更适用于高并发的内存使用环境下。

使用jemalloc

buildroot支持jemalloc的集成,可以很方便的编译出jemalloc动态库。编译好之后,运行应用程序之前,定义LD_PRELOAD=/usr/lib/libjemalloc.so,应用使用的内存分配器就改为jemalloc了。经测试,使用jemalloc时,系统内存使用效率大大提高了,有点不敢相信啊,ScT进程的虚拟内存从494M直接降到了58M,当时我竟然相信了;》,后来才发现,ScT在编译的时候,启用了asan的内存泄漏检测功能,最终导致了ScT的虚拟内存飙升到494M,哎,大意了啊,竟然一直以为将近500M的虚拟内存空间竟然是合理的;)

使用malloc_trim定时清理内存

ptmalloc提供了malloc_trim,“release free memory from the top of the heap”,用于释放堆顶的内存,这里说的堆,是基于sbrk分配的内存,堆顶表示当前sbrk指针的位置,释放堆顶,个人理解就是释放掉堆顶以下空闲的内存区域,将物理内存返还给OS。
malloc_trim原型如下:

int malloc_trim(size_t pad);

pad表示留给堆顶的内存空间大小,如果pad为0,表示只保持最小的内存给堆顶。详见,man malloc_trim(3)

malloc_trim是否有效果呢?

通过下面的例子进行测试:
来自网络:https://blog.csdn.net/u013259321/article/details/112031002

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

代码测试:来自网络https://cloud.tencent.com/developer/article/2002948

#include                                                                                                      
#include 
#include 
#include 
#define K (1024)
#define MAXNUM 500000
int main() 
{char *ptrs[MAXNUM];int i;//malloc large block memoryfor (i = 0; i < MAXNUM; ++i) {ptrs[i] = (char *)malloc(1 * K); memset(ptrs[i], 0, 1 * K); }   //never free,only 1B memory leak, what it will impact to the system?//size_t msize = 10 * 1024 * 1024;size_t msize = 1;char *tmp1 = (char *)malloc(msize);memset(tmp1, 0, msize);printf("%s\n", "malloc done.");getchar();printf("%s\n", "start free memory.");for(i = 0; i < MAXNUM; ++i) {free(ptrs[i]);}   printf("%s\n", "large memory free done.");getchar();malloc_trim(0);printf("%s\n", "malloc_trim(0) done.");getchar();return 0;
}

代码原理:

1).首先,申请500M内存,并且使用memset进行初始化,这一步骤不能省略,只有初始化了,系统才会分配物理内存。
2).然后,再申请1B内存,这个字节内存也需要初始化,并且直到程序退出也不释放,这是为了模拟在堆顶保持1B的内存不释放,从而测试malloc_trim是否可以释放掉该字节之下的500M内存。
3).释放500M内存,可以通过free -m查看,物理内存没有释放。
4).调用malloc_trim(0)释放堆顶内存,通过free -m查看,物理内存已经释放了。

结论:

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

使用mmap分配内存

使用sbrk分配的内存块,会被分配给多个地方,需要将内存块全部释放之后,才能将物理内存返回给OS,所以,有人建议使用mllopt(3)函数来配置M_MMAP_THRESHOLD和M_MMAP_MAX来只使用mmap来申请内存。但是,mmap/unmap是系统调用,每次使用都会造成OS性能的下降,所以,对于大块内存可以使用mmap,但是,对于小块内存的申请和释放,使用mmap是得不偿失的。

相关内容

热门资讯

诚信招聘以解人才后顾之忧 转自:衢州日报  ■新闻速递:当前正值毕业季,各类求职招聘活动火热举办。近日,北京、上海、山东、重庆...
你用心听。你会听到春天的声音。... 你用心听。你会听到春天的声音。(用关联词连成一句)如果…………就如果你用心听,你就会听到春天的声音。...
电影《金刚》的场景是真的吗?在... 电影《金刚》的场景是真的吗?在哪里拍的?就是金刚呆的那个岛是真实存在的吗~~还是电脑效果那个岛是真的...
甘肃天水幼儿血铅异常来源查明 转自:衢州日报  新华社兰州7月8日电 8日上午,甘肃省天水市联合调查组公布了当地幼儿血铅异常事件的...
取悦特朗普,泽连斯基决定换掉驻... 转自:上观新闻在日前与特朗普的通话中,泽连斯基同意撤换驻美女大使奥克萨娜·马尔卡罗娃。泽连斯基此举被...
产品如何提供二次开发如何持续更... 产品如何提供二次开发如何持续更新为了提供二次开发和持续更新,产品需要满足以下一些关键因素:1、清晰的...
在视频聊天中,对方看不到我到图... 在视频聊天中,对方看不到我到图像、听不到声音是怎么回事。看不到图像的原因有:1、没有世罩没摄像头。2...
父母断绝关系来威胁我与男友分手... 父母断绝关系来威胁我与男友分手,我应该怎么解决?我觉得你不应该和父母对着干,毕竟他们是生你养你的父母...
商场中的"先小人后君... 商场中的"先小人后君子"意义就是先把丑话说在先的意思
在家里制作点心时没有烤箱,微波... 在家里制作点心时没有烤箱,微波炉可以代替吗?不可以,这是因为微波炉根本就达不到特别高的温度,而且也没...