内存泄漏的解决方案

第一部分:内存泄漏的说明和解决方案的介绍 (转载)

第二部分:实现模拟检查内存泄漏的样例代码 (原创)

内存泄漏的说明和解决方案的介绍

以下文章转载以:https://www.bcdaren.com/557359488538251265/blog_content.html

众所周知,C/C++执行效率高,但难以驾驭,开车一时爽,但稍不留神容易翻车。估计每个C/C++程序员都遭受过内存泄漏的困扰。本文提供一种通过wrap malloc查找memory leak的思路,使得你翻车的时候能够自救,而不至于车毁人亡。=

什么是内存泄漏?

内存泄漏就是动态申请的内存丢失引用,造成没有办法回收它(我知道杠jing要说进程退出前系统会统一回收),相当于在人身上轧个口子,伤口一直流血不止,关键这口子还不知道轧哪儿。

内存泄漏对于客户端应用可能不是什么大事,而对于长久运行的服务器程序则可能是致命的。

隐式释放

Java等傻瓜式编程语言会自动管理内存回收,你只管用,系统会通过引用计数技术跟踪动态分配的每块内存,在合适的时机自动释放掉,这种方式叫隐式释放,相当于车的自动挡。

显式释放

而C/C++需要显式的释放,也就是开发者需要确保malloc/free配对,确保每块申请的内存都恰当的释放掉,相当于车的手动挡。有很多手段可以避免内存泄漏,比如RAII、比如智能指针(大多基于引用计数)、比如内存池。C/C++程序员也是蛮拼的,一直在跟内存泄漏做殊死搏斗。

理论上,只要我们足够小心,在每次申请的时候,都牢记释放,那么这个世界就清净了。但现实往往没有这般美好,比如抛异常了,释放内存的语句执行不到,比如模块之间的指针传递,又或者某菜鸟程序员不小心埋了颗雷,所以,我们必须直面真实的世界,那就是我们会遭遇内存泄漏。

怎么查内存泄漏?

我们可以review代码,double check,结对编程,但从海量代码里找到隐藏的问题,这如同大海捞针,谈何容易啊?兄弟。

所以,我们需要借助工具,比如valgrind,但这些找内存泄漏的工具,往往对你使用动态内存的方式有某种期待,或者说约束,比如常驻内存的对象会被误报出来,然后真正有用的信息会被掩盖在误报的汪洋大海里,所以,很多时候,valgrind根本解决不了日常项目中的问题,并没什么卵用。

很多著名的开源项目,为了能用valgrind跑,都大张旗鼓的修改源代码,从而使得项目符合valgrind的要求,用vargrind跑过没有任何报警叫valgrind干净,这倒也不失为一个一劳永逸的办法。

既然这些个玩意儿都中看不中用,所以,求人不如求己,还得自力更生,多大点事儿。

operator new/delete重载和hook malloc/free

可以通过operator new/delete,operator new[]/delete[]重载,但这里有很细致的功夫,你需要全面了解,而不是贸然行动,建议看看Effective C++,对operator new系列操作符重载有专门的阐述。

你也可以hook malloc、free等c编程接口。

你还可以开启ptmalloc的调试功能,它有时候也能管点用。

什么是动态内存分配器?

动态内存分配器是介于kernel跟应用程序之间的一个函数库,linux glibc提供的动态内存分配器叫ptmalloc,因为抱了linux的大腿,故而是应用最广泛的动态内存分配器。

从kernel角度看,动态内存分配器属于应用程序层;而从应用程序的角度看,动态内存分配器属于系统层。到底属于哪一层,这取决于你的身份和角度。

 

应用程序可以通过mmap系统调用直接向系统申请动态内存,也可以通过动态内存分配器的malloc接口分配内存,而动态内存分配器会通过sbrk、mmap向系统分配内存,所以应用程序通过free释放的内存,并不一定会真正返还给系统,它也有可能被动态内存分配器缓存起来。

所以当你malloc/free配对得很好,但通过top命令去看进程的内存占用,还是很高,你不必感到惊讶。

google有自己的动态内存分配器tcmalloc,另外jemalloc也是著名的动态内存分配器,他们有不同的性能表现,也有不同的缓存和分配策略。你可以用它们替换linux系统glibc自带的ptmalloc。

new/delete跟malloc/free的关系

new是c++的用法,比如Foo *f = new Foo,其实它分为3步。

  1. 通过operator new()分配sizeof(Foo)的内存,最终通过malloc分配。
  2. 在新分配的内存上构建Foo对象。
  3. 返回新构建的对象地址。

new=分配内存+构造+返回,而delete则是等于析构+free。

所以搞定malloc、free就是从根本上搞定动态内存分配。

chunk

每次通过malloc返回的一块内存叫一个chunk,动态内存分配器是这样定义的,后面我们都这样称呼。

wrap malloc

gcc支持wrap,即通过传递-Wl,--wrap,malloc的方式,可以改变调用malloc的行为,把对malloc的调用链接到自定义的__wrap_malloc(size_t)函数,而我们可以在__wrap_malloc(size_t)函数的实现中通过__real_malloc(size_t)真正分配内存,而后我们可以做搞点小动作。

同样,我们可以wrap free。

malloc跟free是配对的,当然也有其他相关API,比如calloc、realloc、valloc,这些都是细节,根本上还是malloc和free,比如realloc就是malloc + free的组合。

怎么去定位内存泄漏呢?

我们会malloc各种不同size的chunk,也就是每种不同size的chunk会有不同数量,如果我们能够跟踪每种size的chunk数量,那就可以知道哪种size的chunk在泄漏。很简单,如果该size的chunk数量一直在增长,那它很可能泄漏。

光知道某种size的chunk泄漏了还不够,我们得知道是哪个调用路径上导致该size的chunk被分配,从而去检查是不是正确释放了。

怎么跟踪到每种size的chunk数量?

我们可以维护一个全局 unsigned int malloc_map[1024 * 1024]数组,该数组的下标就是chunk的size,malloc_map[size]的值就对应到该size的chunk分配量。

这等于维护了一个chunk size到chunk count的映射表,它足够快,而且可以覆盖到0 ~ 1M大小的chunk的范围,它已经足够大了,试想一次分配一兆的块已经很恐怖了,可以覆盖到大部分场景。

那大于1M的块怎么办呢?我们可以通过log的方式记录下来。

在__wrap_malloc里,++malloc_map[size]

在__wrap_free里,--malloc_map[size]

如此一来,我们便通过malloc_map记录了各size的chunk的分配量。

如何知道释放的chunk的size?

不对,free(void *p)只有一个参数,我如何知道释放的chunk的size呢?怎么办?

我们通过在__wrap_malloc(size_t)的时候,分配8+size的chunk,也就是额外分配8字节,用起始的8字节存储该chunk的size,然后返回的是(char*)chunk + 8,也就是偏移8个字节地址,返回给调用malloc的应用程序。

这样在free的时候,传入参数void* p,我们把p往前移动8个字节,解引用就能得到该chunk的大小,而该大小值就是之前在__wrap_malloc的时候设置的size。

好了,我们真正做到记录各size的chunk数量了,它就存在于malloc_map[1M]的数组中,假设64个字节的chunk一直在被分配而没有被正确回收,最终会表现在malloc_map[size]数值一直在增长,我们觉得该size的chunk很有可能泄漏,那怎么定位到是哪里调用过来的呢?

如何记录调用链?

我们可以维护一个toplist数组,该数组假设有10个元素,它保存的是chunk数最大的10种size,这个很容易做到,通过对malloc_map取top 10就行。

然后我们在__wrap_malloc(size_t)里,测试该size是不是toplist之一,如果是的话,那我们通过glibc的backtrace把调用堆栈dump到log文件里去。

注意:这里不能再分配内存,所以你只能使用backtrace,而不能使用backtrace_symbols,这样你只能得到调用堆栈的符号地址,而不是符号名。

如何把符号地址转换成符号名,也就是对应到代码行呢?答案是addr2line。

addr2line

addr2line工具可以做到,你可以追查到调用链,进而定位到内存泄漏的问题。

至此,恭喜你,你已经get到了整个核心思想。

 

当然,实际项目中,我们做的更多,我们不仅仅记录了toplist size,还记录了各size chunk的增量toplist,会记录大块的malloc/free,会wrap更多的API。

总结

通过wrap malloc/free + backtrace + addr2line,你就可以定位到内存泄漏了。

美好的时间过得太快,又是时候说byebye!

 

实现模拟检查内存泄漏的样例代码:

#ifndef __MALLOC_MAP_H__
#define __MALLOC_MAP_H__

#ifndef NULL
#define	NULL	(void *)0
#endif

#define TOPLIST_SIZE	(10)

struct __malloc_map {
	size_t	dump_boundary;							/**< 出现dump时的动态内存块数量边界 */
	struct __malloc_node *ptr_list;					/**< 维护动态分配的内存链表 */
	struct __malloc_node *toplist[TOPLIST_SIZE]; 	/**< 维护最大内存块数量top的前10个 */
};

extern void *__wrap_malloc(size_t size);

extern void *__wrap_calloc(size_t size);

extern void *__wrap_realloc(void *mem_address, size_t newsize);

extern void __wrap_free(void *ptr);

extern void malloc_map_dump(void);

extern void malloc_map_show_toplist(void);

extern int malloc_map_set_dump_boundary(size_t dump_boundary);

#define malloc(size)					__wrap_malloc(size)
#define calloc(size)					__wrap_calloc(size)
#define realloc(mem_address, newsize)	__wrap_realloc(mem_address, newsize)
#define free(size)						__wrap_free(size)

#endif /* __MALLOC_MAP_H__ */

#include <execinfo.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

struct __malloc_node {
	struct __malloc_node *ptr_next;	
	size_t	chunk_count;
	size_t	chunk_size;
};

#include "mallocmap.h"

#define BT_BUF_SIZE 	(100)
#define MALLOC_MAP_INIT	\
	{ .ptr_list = NULL, .dump_boundary = 0, .toplist = {NULL} }

static int sg_interrupted = 0;
static struct __malloc_map sg_malloc_map = MALLOC_MAP_INIT;

void *__real_malloc(size_t size);
void __real_free(void *ptr);

char *get_name_by_pid(pid_t pid, char *task_name)
{
	FILE* fp = NULL;
    char proc_pid_path[1024] = {0};
    char buf[1024] = {0};

    sprintf(proc_pid_path, "/proc/%d/status", pid);
    fp = fopen(proc_pid_path, "r");
	if (NULL == fp) {
		return "";
	}
	
    if (NULL != fgets(buf, 1024 - 1, fp)) {
         sscanf(buf, "%*s %s", task_name);
    }
    fclose(fp);

	return task_name;
}


/* 若找到,则存放到ptr_cur指针中,返回ptr_cur的之前指针 */
static struct __malloc_node *malloc_node_find_chunk_size(struct __malloc_node *ptr_list, 
					size_t chunk_size, struct __malloc_node **ptr_cur)
{
	struct __malloc_node *ptr_pre = ptr_list;
	struct __malloc_node *ptr_tmp = ptr_list;

	while (NULL != ptr_tmp) {
		if (ptr_tmp->chunk_size == chunk_size) {
			(*ptr_cur) = ptr_tmp;
			break;
		}
		ptr_pre = ptr_tmp;
		ptr_tmp = ptr_tmp->ptr_next;		
	}

	return ptr_pre;
}

static int malloc_node_insert(struct __malloc_node **pptr_list, 
				size_t chunk_count, size_t chunk_size)
{
	struct __malloc_node *ptr_node = NULL;
	
	ptr_node = (struct __malloc_node *)__real_malloc(sizeof(struct __malloc_node));
	if (NULL == ptr_node) {
		return -1;
	}
	
	ptr_node->chunk_count = chunk_count;
	ptr_node->chunk_size  = chunk_size;
	ptr_node->ptr_next	 = NULL;
	(*pptr_list) = ptr_node;
	
	return 0;
}

static void malloc_node_bubble_sort(struct __malloc_node *ptr_list)
{
	struct __malloc_node *p = NULL;
	struct __malloc_node *q = NULL;
	struct __malloc_node tmp;
	
	for (p = ptr_list; p->ptr_next != NULL; p = p->ptr_next){
        for (q = p; q->ptr_next != NULL; q = q->ptr_next){
            if (q->chunk_count < q->ptr_next->chunk_count){
                tmp.chunk_count = q->chunk_count;
				tmp.chunk_size  = q->chunk_size;
				q->chunk_count = q->ptr_next->chunk_count;
				q->chunk_size = q->ptr_next->chunk_size;
				q->ptr_next->chunk_count = tmp.chunk_count;
				q->ptr_next->chunk_size = tmp.chunk_size;
            }
        }
    }
}

void malloc_map_dump(void)
{
	int idx = 0;
	int nptrs = 0;
	char task_name[1024] = {0};
	char execmd[BT_BUF_SIZE] = {0};
	void *buffer[BT_BUF_SIZE];

	/* buffer里面存放的是每层调用函数的返回地址 */
	nptrs = backtrace(buffer, BT_BUF_SIZE);
	printf("******** malloc_map_dump (PID:%u) *********\n", (unsigned int)getpid());
	for (idx = 0; idx < nptrs; idx++) {
		printf("backtrace(%d) addresses: %p\n", idx, buffer[idx]);
		snprintf(execmd, sizeof(execmd), "./addr2line -e %s %p -f", \
			get_name_by_pid(getpid(), task_name), buffer[idx]);
		system(execmd);
	}	
	printf("******** malloc_map_dump (end) *********\n");
	exit(-1);
	
	return;
}

void malloc_map_dump_fd(int fd)
{
	int nptrs = 0;	
	void *buffer[BT_BUF_SIZE];

	/* buffer里面存放的是每层调用函数的返回地址 */
	nptrs = backtrace(buffer, BT_BUF_SIZE);

	if (nptrs > 0) {
		backtrace_symbols_fd(buffer, BT_BUF_SIZE, fd);
	}
	
	return;
}

static int malloc_map_del_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
	struct __malloc_node *ptr_cur = NULL;
	struct __malloc_node *ptr_pre = NULL;

	ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
	if (NULL == ptr_cur || NULL == ptr_pre) {				
		fprintf(stderr, "ptr_cur or ptr_pre is null, code=%d (%s)", errno, strerror(errno));
		return -1;
	}
	
	if (ptr_cur->chunk_count > 0) {
		ptr_cur->chunk_count--;
	}
	if (0 == ptr_cur->chunk_count && NULL != ptr_pre) {
		if (ptr_pre == ptr_cur) {
			ptr_map->ptr_list = NULL;
		} else {
			ptr_pre->ptr_next = ptr_cur->ptr_next;
		}
		__real_free(ptr_cur);
	}

	return 0;
}

static int malloc_map_add_list(struct __malloc_map *ptr_map, size_t chunk_size)
{
	struct __malloc_node *ptr_cur = NULL;
	struct __malloc_node *ptr_pre = NULL;

	if (NULL == ptr_map->ptr_list) {
		malloc_node_insert(&(ptr_map->ptr_list), 1, chunk_size);		
	} else {
		ptr_pre = malloc_node_find_chunk_size(ptr_map->ptr_list, chunk_size, &ptr_cur);
		if (NULL != ptr_cur) { // 找到与chunk_size相同的指针
			ptr_cur->chunk_count++;
			// 分配的chunk数量大于dump_boundary则dump出栈调用流程
			if (ptr_cur->chunk_count > ptr_map->dump_boundary) {
				malloc_map_dump();
			}
		} else if (NULL != ptr_pre) { // 没找到插入到链表尾部
			malloc_node_insert(&(ptr_pre->ptr_next), 1, chunk_size);
		}
	}

	return 0;
}

static void malloc_map_update_toplist(struct __malloc_map *ptr_map)
{
	int	idx = 0;
	struct __malloc_node *p = NULL;
	
	malloc_node_bubble_sort(ptr_map->ptr_list);
	for (idx = 0, p = ptr_map->ptr_list; idx < TOPLIST_SIZE && NULL != p; idx++, p = p->ptr_next) {
		ptr_map->toplist[idx] = p;
		//printf("__wrap_flush_toplist idx=%u, chunk_size=%u\n", idx, p->chunk_size);
	}
}

void malloc_map_show_toplist(void)
{
	int 	idx = 0;
	char	task_name[1024] = {0};
	struct __malloc_map *ptr_map = NULL;

	ptr_map = &sg_malloc_map;
	if (ptr_map->ptr_list) {
		system("clear");
		printf("toplist(boundary:%d):\n", ptr_map->dump_boundary);
		printf("PID\tADDRESS\tSIZE\tCOUNT\tPNAME\n");
		for (idx = 0; idx < TOPLIST_SIZE; idx++) {
			if (NULL != ptr_map->toplist[idx]) {
				printf("%u\t%p\t%u\t%u\t%s\n", (int)getpid(), \
					ptr_map->toplist[idx] + sizeof(size_t), \
					ptr_map->toplist[idx]->chunk_size - sizeof(size_t), \
					ptr_map->toplist[idx]->chunk_count,
					get_name_by_pid(getpid(), task_name));
			}
		}
		printf("\n");
	}
}

int malloc_map_set_dump_boundary(size_t dump_boundary)
{
	sg_malloc_map.dump_boundary = dump_boundary;
	
	return dump_boundary;
}

void *__wrap_malloc(size_t size)
{
	size_t chunk_size = 0;
	void *ptr_chunk = NULL;

	chunk_size = size + sizeof(size_t);
	if (NULL == (ptr_chunk = __real_malloc(chunk_size))) {
		fprintf(stderr, "ptr_chunk is null, code=%d (%s)", errno, strerror(errno));
		return NULL;
	}

	memcpy(ptr_chunk, &size, sizeof(size_t));
	//printf("__wrap_malloc size=%d, (size_t *)ptr_chunk=%d\n", size, *(size_t *)ptr_chunk);
	malloc_map_add_list(&sg_malloc_map, chunk_size);
	malloc_map_update_toplist(&sg_malloc_map);
		
	return ptr_chunk + sizeof(size_t);
}

void *__wrap_calloc(size_t size)
{
	void *ptr_data = NULL;
	
	if (NULL == (ptr_data = __wrap_malloc(size))) {
		return NULL;
	}

	memset(ptr_data, 0x00, size);
		
	return ptr_data;
}

void *__wrap_realloc(void *mem_address, size_t newsize)
{
	size_t real_size = 0;
	void *ptr_data = NULL;

	real_size = *(size_t *)(mem_address - sizeof(size_t));
	if (NULL == (ptr_data = __wrap_malloc(newsize))) {
		return NULL;
	}

	memcpy(ptr_data, mem_address, real_size);
		
	return ptr_data;
}

void __wrap_free(void *ptr)
{
	size_t chunk_size = 0;
	void *ptr_chunk = NULL;
	
	if (NULL != ptr) {
		ptr_chunk = ptr - sizeof(size_t);
		chunk_size = (*(size_t *)(ptr_chunk)) + sizeof(size_t);
		__real_free(ptr_chunk);
		malloc_map_del_list(&sg_malloc_map, chunk_size);
	}
}

static void abnormal_andler(int signo)
{
	printf("abnormal_andler signo:%d\n", signo);
	sg_interrupted = 1;	
	malloc_map_dump();
	exit(0);
}


//

void test_func_xxx(void)
{
	int idx = 0;
	char *ptr = NULL;
	for (idx = 0; idx < 20; idx++) {
		ptr = __wrap_malloc(1024 + idx);
		if (3 == idx) {
			__wrap_free(ptr);
		}
		if (1 == idx) {
			__wrap_free(ptr);
		}
	}
}

void test_func_yyy(void)
{
	int idx = 0;
	char *ptr = NULL;
	for (idx = 0; idx < 20; idx++) {
		ptr = __wrap_malloc(1024 + idx);
		if (5 == idx) {
			__wrap_free(ptr);
		}
		if (1 == idx) {
			__wrap_free(ptr);
		}
	}
}

int testmain(void)
{
	printf("wrap_malloc main\n");

	malloc_map_set_dump_boundary(8);

	test_func_xxx();
	test_func_yyy();

	signal(SIGSEGV, abnormal_andler);
	signal(SIGABRT, abnormal_andler);

	while (!sg_interrupted) {
		malloc_map_show_toplist();
		test_func_xxx();

		__wrap_malloc(1024);
		usleep(1000*3000);
	}

	return 0;
}

//

CFLAGS	:= -g -Wall -D__GNU__ $(MYCFLAGS) -D_GNU_SOURCE -D__USE_XOPE -mfloat-abi=hard
WRAPFUNC := -rdynamic -funwind-tables -ffunction-sections -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=calloc -Wl,--wrap=realloc
CFLAG_TARGET := $(CFLAGS)
CFLAG_TARGET += -Wl,-rpath-link $(TARGET_LIB_DIR)

CFLAGS += -I./include/ 
SOURCES := $(wildcard *.c) $(wildcard $(DIR_EXBOARD)/*.c $(DIR_DB)/*.c \
	$(DIR_RS485)/*.c $(DIR_XYBASE)/*.c $(DIR_NET)/*.c $(DIR_READER)/*.c ) 
OBJS := $(patsubst %.c,%.o,$(SOURCES))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
$(patsubst %.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS += -MD

TARGET := mallocmap

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAG_TARGET) -o $(TARGET_SERVICE_DIR)/$(TARGET) $(OBJS) $(WRAPFUNC) $(LIB)
	cp $(TARGET_SERVICE_DIR)/$(TARGET) $(DEST_PATH)/$(TARGET)_$(LOGNAME) -rf
	
$(OBJS):  %.o: %.c $(DEP)
	$(CC) $(CFLAGS) $(WRAPFUNC) -c $< -o $(subst ../,,$@)

clean:
	rm -rf $(subst ../,,$(OBJS))
	rm -rf *.gcno

cleanall:
	rm -rf $(subst ../,,$(OBJS))
	rm -rf $(TARGET_SERVICE_DIR)/$(TARGET)
	rm -rf *.h~
	rm -rf *.c~
	rm -rf *.d

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值