目的
目前线上代码有一定的内存泄漏问题,大多数情况下这种bug都难以追踪定位,因此想开发一个内存监测小工具。
需要两种监测方式。一种是全局监测,纪录每一次内存的分配和释放活动;另一种是较为轻量级的监测,只监测部分疑似存在泄漏的code。
内存监测需要hack进内存分配和释放相关的代码,监测其每次的活动。
方法选择
1.重载new/delete
首先想到的是对管理动态分配内存的函数进行改写和监测。而new 和 delete很容易重写,且据说也是线上用的最多的动态分配内存方式。
重载 ::operator new() 的理由Effective C++ 第三版第 50 条列举了定制 new/delete 的几点理由:
-
- 检测代码中的内存错误
- 优化性能
- 获得内存使用的统计数据
然而,这么做的缺点也有很多,参见《不要重载全局new》http://www.360doc.com/content/12/1211/17/9200790_253442412.shtml 主要问题是与c库的兼容问题
2.从malloc,free入手
既然不选择重载new/delete,就只能从malloc free入手了。且new delete的底层也是由malloc free实现的。这里大约由如下几种
1.#define malloc(x ) debug_malloc(x,__file__,__line__)
优点:方便
缺点:不能替换系统函数使用到的malloc,如 sprintf new 等使用的还是系统malloc
2.修改glibc或(libc.so)
缺点:有很多函数,影响整个系统,不只是当前代码使用
3.__malloc_hook
优点:只要你在程序中写上”__malloc_hook = my_malloc_hook;”,之后的malloc调用都会使用my_malloc_hook函数,方便易行。
缺点:但是这组调试变量不是线程安全的,当你想用系统malloc的时候不得不把他们改回来,多线程调用就得上锁了。因此方案不很适用于系统内存优化,勉强用来简单管理线程内存使用。
详细用法: http://linux.die.net/man/3/__malloc_hook
/* Prototypes for our hooks. */ static void my_init_hook(void); static void *my_malloc_hook(size_t, const void *); /* Variables to save original hooks. */ static void *(*old_malloc_hook)(size_t, const void *); /* Override initializing hook from the C library. */ void (*__malloc_initialize_hook) (void) = my_init_hook; static void my_init_hook(void) { old_malloc_hook = __malloc_hook; __malloc_hook = my_malloc_hook; } static void * my_malloc_hook(size_t size, const void *caller) { void *result; /* Restore all old hooks */ __malloc_hook = old_malloc_hook; /* Call recursively */ result = malloc(size); /* Save underlying hooks */ old_malloc_hook = __malloc_hook; /* printf() might call malloc(), so protect it too. */ printf("malloc(%u) called from %p returns %p\n", (unsigned int) size, caller, result); /* Restore our own hooks */ __malloc_hook = my_malloc_hook; return result; } |
4.dlysm
调用dlsym来对系统malloc函数进行存取,保存为sys_malloc。然后就可以重写malloc了,在重写的malloc里面调用sys_malloc来进行真正的内存分配。
详细用法及注意事项:http://www.slideshare.net/tetsu.koba/tips-of-malloc-free
功能: 根据动态链接库操作句柄与符号,返回符号对应的地址。 包含头文件: #include<dlfcn.h> 函数定义: void* dlsym(void* handle,const char*symbol) 函数描述: 根据 动态链接库 操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。 handle:由dlopen打开动态链接库后返回的指针; symbol:要求获取的函数或全局变量的名称。 返回值: void* 指向函数的地址,供调用使用。 |
使用DLYSM重写系统的内存管理函数
1.函数原型设计
extern "C" void* malloc(size_t size) { void * ptr=sys_malloc(size); handle extra behavior... return ptr; } |
使用sys_malloc来进行真正的内存分配,然后做一些记录等额外的操作,最后返回指针。
2.存储系统真正内存分配释放函数
static bool performance_enabled_ = false; static void* (*sys_malloc)(size_t) = 0; static void* (*sys_realloc)(void*,size_t) = 0; static void* (*sys_calloc)(size_t,size_t) = 0; static void (*sys_free)(void*) = 0; static void initialize_functions() { sys_malloc = reinterpret_cast<void*(*)(size_t)>(dlsym(RTLD_NEXT, "malloc")); sys_realloc = reinterpret_cast<void*(*)(void*,size_t)>(dlsym(RTLD_NEXT, "realloc")); sys_calloc = reinterpret_cast<void*(*)(size_t,size_t)>(dlsym(RTLD_NEXT, "calloc")); sys_free = reinterpret_cast<void(*)(void*)>(dlsym(RTLD_NEXT, "free")); } |
进程可以使用 dlsym(3C) 获取特定符号的地址。此函数采用句柄和符号名称,并将符号地址返回给调用方。特殊句柄 RTLD_NEXT 允许从调用方链接映射列表中的下一个关联目标文件获取符号。由于我们重写了malloc,RTLD_NEXT就指向了系统的malloc。这样,sys_malloc,sys_realloc,sys_calloc,sys_free则存储了系统的内存管理函数。
3.初始化问题
上面两条描述了malloc重写的核心操作。我们可以在第一次使用malloc的时候对sys_malloc进行初始化。
extern "C" void* malloc(size_t size) { if(sys_malloc==0) initialize_functions(); void * ptr=sys_malloc(size); handle extra behavior... return ptr; } |
然而,调用dlsym来对系统malloc函数进行存取的时候,会调用calloc,如果calloc也用dlsym进行重载了,会造成循环调用。在你调用dlsym这个函数时,dlsym会调用dlerror,dlerror会调用calloc,calloc要调用malloc,而你的malloc正在初始化等待dlsym返回中,于是死循环了。
因此在第一次调用malloc或者calloc的时候可以分配一段静态的内存供dlsym使用以解决这个问题。
char tmpbuff[1024]; unsigned long tmppos = 0; unsigned long tmpallocs = 0; extern "C" void* malloc(size_t size) { static bool is_initializing = false; if(sys_malloc == 0) { if(!is_initializing) { is_initializing = true; initialize_functions(); is_initializing = false; } else { if(tmppos+size<sizeof(tmpbuff)) { void *retptr = tmpbuff + tmppos; tmppos += size; ++tmpallocs; return retptr; } else exit(1); } } void* ptr = sys_malloc(size); handle extra behaviors.... return ptr; } |
4.循环调用的问题
当extra behavior中涉及到动态内存分配和释放的行为时,就会出现循环调用。 extra behavior->malloc->extra behavior->malloc->....因此我们需要使用一个flag来标志需要记录的外部变量。通过此flag来判断是否进行额外操作。这里使用了thread local storage的标志 __thread, 用于标识每个线程的is_external情况。
static __thread bool is_external = true; extern "C" void* malloc(size_t size) { static bool is_initializing = false; if(sys_malloc == 0) { if(!is_initializing) { is_initializing = true; initialize_functions(); is_initializing = false; } else { if(tmppos+size<sizeof(tmpbuff)) { void *retptr = tmpbuff + tmppos; tmppos += size; ++tmpallocs; return retptr; } else exit(1); } } void* ptr = sys_malloc(size); if(is_external) { is_external=false; handle extra behaviors.... is_external=true; } return ptr; } |
5.辅助函数
使用 int backtrace(void **buffer,int size) 函数得到当前线程的调用堆栈。
使用 size_t malloc_usable_size (void *ptr) 函数获得该地址指针指向的内存大小。
note: malloc(size) calloc(size,cnt) realloc(ptr,size)等函数分配的内存大小不一定是调用函数的时候赋予的size的大小,主要是由于地址对齐的考虑。所以需要用malloc_usable_size来获得真正的内存大小。
Monitor 设计
对malloc,calloc,realloc,free使用 dlsym来对系统函数进行存取的方式进行重写。使得内存分配函数除了分配内存还能做一些记录内存分配的操作。
monitor主要数据结构如下
//record general performance info struct General_Performance { int64_t used_memory_; std::vector<void *> stack_trace_; }; //record general performance info by address struct General_Performance_Map: public std::unordered_map<uint64_t, General_Performance> { }; //record specific performance info struct Specific_Performance { size_t count_; int64_t duration_; //mili seconds int64_t hold_memory_; //the memory change size during a monitor life time int64_t allocated_memory_;//the memory allocation size during a monitor life time int64_t start_time_; //mili seconds int64_t end_time_; //mili seconds General_Performance_Map detail_; }; //record specific performance info by guard's tag struct Specific_Performance_Map: public std::unordered_map<Ads_String, Specific_Performance> { }; //record the specific & general info for each thread struct Thread_Performance_Info { int guards_num_; //the number of guards in current thread General_Performance_Map general_performance_map_;//record the general performance by address std::list<void*> stack_guard_father_call_names_;//record the father function called Guard as a stack std::list<Specific_Performance> stack_specific_performance_;//record the specific performance as a stack, each only manipulate the back element, Specific_Performance_Map specific_performance_map_;//record the specific performance by guard's tag Thread_Performance_Info(); ~Thread_Performance_Info();//do merge work when destruct a thread info }; //when a guard object exist, the corresponding specific monitor will be at working status struct Guard { Ads_String tag_; int64_t start_time_; int64_t end_time_; Guard(const Ads_String& tag); ~Guard(); }; |
1.全局的memory leak detection
对每一次内存的分配和释放都进行记录,并使用backtrace进行调用跟踪,获得全局的内存分配信息,需要通过编译选项打开.
1.1在每个线程创建一个unordered_map<int64, general_info> ,这里key是用于内存分配的指针,value是这个指针的当前大小以及导致这次内存分配/释放的call stack
1.2每次内存操作的时候更新该线程的map信息
1.3线程退出时,把当前的map信息merge到全局的general info map中
2.specific的memory leak detection
使用宏定义,展开成构造一个Guard,在Guard的对象的作用域内统计分配内存大小,起止时间,进入次数等信息。利用函数的栈调用形式,Guard可以使用栈结构存储,只操作栈顶元素,在需要pop时把信息merge到新的栈顶
2.1通过PERFORMANCE_MONITOR的宏展开成为函数名的guard结构体,并保存当前的时间信息入栈stack_specific_performance
2.2每次内存操作查看stack_specific_performance是否为空,不为空则记录内存的变化信息以及call stack到栈顶
2.3guard结构体生命周期结束析构的时候,记录当前时间,计算duration,start time,end time等信息,并弹出栈顶。如果栈不为空,则把当前的specific info 合并到新的栈顶元素。
3.性能优化
3.1多线程优化
工具的使用环境是多线程的,如果每次内存信息的记录都merge到全局的变量,必然要给变量加锁以免冲突,这会极大的降低效率。 因此我们需要构造记录监测信息的线程内变量,在线程退出的时候,把这些变量merge到全局变量中。这样可以极大的减少锁的使用。
因此可以使用__thread_local Thread_Performance_Info>的形式构造线程变量,并且在Thread_Performance_Info的析构函数中加入线程变量的merge行为。因为TSS变量在析构的时候,会调用类型的析构函数。这样就满足了在线程析构的时候才merge到全局变量的要求。