內存泄漏的解決方案

第一部分:內存泄漏的介紹和解決方案的介紹 (轉載)

第二部分:實現模擬檢查內存泄漏的樣例代碼 (原創)

內存泄漏的介紹和解決方案的介紹

以下文章轉載以: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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章