本文所描述的内存调试过程,主要是记录最近项目里面遇到的一个内存使用问题。过程大概是,测试软件稳定性时,发现系统内存随着时间的变化,会不断的增长,并且不会恢复。由于怀疑是,应用程序出现了内存泄漏,所以开启了针对于内存泄漏的分析、调试,过程中使用了程序功能模块隔离法、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的基本原理如下:
ptmalloc的几个缺点:
内存回收策略导致内存不能真正释放,并返还给OS,比如,ptmalloc内存紧缩策略,如果通过sbrk分配的大块儿内存,多次分给了不同的部分,只有当后分配的内存释放之后,之前分配的内存块才能释放,这样会造成很多本来没有用的内存块不能返还给OS。
内存管理策略容易导致内存碎片化。
对于多核、多线程支持不友好。
内存malloc和free效率不高。
相比之下,jemalloc和tcmalloc对于ptmalloc这些问题进行了改进,降低了内存碎片化,提高了多核、多线程下内存管理性能,其中tcmalloc更适用于高并发的内存使用环境下。
buildroot支持jemalloc的集成,可以很方便的编译出jemalloc动态库。编译好之后,运行应用程序之前,定义LD_PRELOAD=/usr/lib/libjemalloc.so,应用使用的内存分配器就改为jemalloc了。经测试,使用jemalloc时,系统内存使用效率大大提高了,有点不敢相信啊,ScT进程的虚拟内存从494M直接降到了58M,当时我竟然相信了;》,后来才发现,ScT在编译的时候,启用了asan的内存泄漏检测功能,最终导致了ScT的虚拟内存飙升到494M,哎,大意了啊,竟然一直以为将近500M的虚拟内存空间竟然是合理的;)
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)后这些内存空洞仍然会归还操作系统。
使用sbrk分配的内存块,会被分配给多个地方,需要将内存块全部释放之后,才能将物理内存返回给OS,所以,有人建议使用mllopt(3)函数来配置M_MMAP_THRESHOLD和M_MMAP_MAX来只使用mmap来申请内存。但是,mmap/unmap是系统调用,每次使用都会造成OS性能的下降,所以,对于大块内存可以使用mmap,但是,对于小块内存的申请和释放,使用mmap是得不偿失的。