记录一次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是得不偿失的。

相关内容

热门资讯

求经典台词和经典旁白 求经典台词和经典旁白谁有霹雳布袋戏里的经典对白和经典旁白啊?朋友,你尝过失去的滋味吗? 很多人在即将...
小王子第二章主要内容概括 小王子第二章主要内容概括小王子第二章主要内容概括小王子第二章主要内容概括
爱情睡醒了第15集里刘小贝和项... 爱情睡醒了第15集里刘小贝和项天骐跳舞时唱的那首歌是什么谢谢开始找舞伴的时候是林俊杰的《背对背拥抱》...
世界是什么?世界是什么概念?可... 世界是什么?世界是什么概念?可以干什么?物质的和意识的 除了我们生活的地方 比方说山 河 公路 ...
全职猎人中小杰和奇牙拿一集被抓 全职猎人中小杰和奇牙拿一集被抓动画片是第五十九集,五十八集被发现,五十九被带回基地,六十逃走
“不周山”意思是什么 “不周山”意思是什么快快快快......一座山,神话里被共工撞倒了。
《揭秘》一元一分15张跑得快群... 一元一分麻将群加群主微【ab120590】【tj525555】 【mj120590】等风也等你。喜欢...
玩家必看手机正规红中麻将群@2... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
始作俑者15张跑的快群@24小... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《重大通知》24小时一元红中麻... 加V【ab120590】【tj525555】【mj120590】红中癞子、跑得快,等等,加不上微信就...
盘点一下正规一块红中麻将群@2... 一元一分麻将群加群主微:微【ab120590】 【mj120590】【tj525555】喜欢手机上打...
(免押金)上下分一元一分麻将群... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
[解读]正规红中麻将跑的快@群... 微信一元麻将群群主微【ab120590】 【tj525555】【mj120590】一元一分群内结算,...
《普及一下》全天24小时红中... 微【ab120590】 【mj120590】【tj525555】专业麻将群三年房费全网最低,APP苹...
优酷视频一元一分正规红中麻将... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
《火爆》加入附近红中麻将群@(... 群主微【ab120590】 【mj120590】【tj525555】免带押进群,群内跑包包赔支持验证...
《字节跳动》哪里有一元一分红中... 1.进群方式-[ab120590]或者《mj120590》【tj525555】--QQ(QQ4434...
全网普及红中癞子麻将群@202... 好运连连,全网推荐:(ab120590)(mj120590)【tj525555】-Q号:(QQ443...
「独家解读」一元一分麻将群哪里... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...
通知24小时不熄火跑的快群@2... 1.进群方式《ab120590》或者《mj120590》《tj525555》--QQ(4434063...