项目中有的时候会产生内存泄漏,以往的经验,检测工具更倾向于使用LeakTracer进行检测泄漏问题,但是直接使用会有些问题,比如堆栈不全都是??等问题,该专题希望自己能够坚持将LeakTracer的源码梳理清楚,以供后续的定制化开发,也可以根据这个源码的学历,扩充下自己的知识面
从哪里开始,随便吧,从下面这个函数
static void __attribute__ ((constructor)) MemoryTraceOnInit(void);
static void __attribute__ ((destructor)) MemoryTraceOnExit(void);
重点是__attribute__ ((constructor))和__attribute__ ((destructor))
constructor
参数让系统执行main()
函数之前调用函数(被__attribute__((constructor))
修饰的函数).同理, destructor
让系统在main()
函数退出或者调用了exit()
之后,调用我们的函数.带有这些修饰属性的函数,对于我们初始化一些在程序中使用的数据非常有用
测试发现,无论这个函数定义在main.cpp里面,还是另外一个test.cpp,只要编译或者链接到了执行程序,都会在执行main之前执行上述函数
一、初始化
1. MemoryTraceOnInit从名字可以看出,是内存检测的初始化逻辑
新知识:pthread_once
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次
可以看到的确是PTHREAD_ONCE_INIT
MemoryTrace::init_no_alloc_allowed函数在做什么呢?
不急,一行一行分析
108行 libc_alloc_func_t是个结构体:
111行遍历一个该结构体的数组
第 一个字符串,表示是什么函数,第二个参数就是libc的标准函数,第三个是什么?
那lt_malloc来说,就是定义了一个返回指针的函数指针,感觉这个地方有点绕,还没有理解这么设计的初衷
返回前面的代码,for循环其实就是将libc的标准函数指针设置给结构体第三个参数
问题?else不会执行吗,else里面是干嘛的?
函数定义 void *dlsym(void *handle, const char* symbol);
handle是由dlopen打开动态链接库后返回的指针,symbol就是要求获取的函数的名称。dlsym函数的返回值是void*,指向要查找的函数symbol的地址,供调用使用
使用RTLD_NEXT参数找到的的函数指针就是后面第一次出现这个函数名的函数指针。
我们可能会链接多个动态库,不同的动态库可能都会有symbol这个函数名,那么使用RTLD_NEXT参数后dlsym返回的就是第一个遇到(匹配上)symbol这个符号的函数的函数地址。进一步的我们使用dlsym的返回调用的也就是这个第一个匹配上的函数了
那感觉上面代码的意思是,要么调用__libc_malloc要呢调用malloc
继续后面的代码
这个感觉在创建一个实例,但是这个创建方式,以前也没有用过,具体如下
上述代码中的s_memoryTrace_instance,是下面的定义(一个对象大小的char数组)
然后里面这个char数组强制转换成MemoryTrace的一个静态对象指针,然后调用构造函数
这个过程应该是说,C++里面对象的new其实分成了“内存创建” + “构造函数调用”,这里只是new的过程拆开做了,为什么?
是不是重载new之后,new就不能像上述流程一样工作了,所以创建对象必须拆分?
继续后面的代码,pthread_key_create是干嘛的?
键、键析构函数的创建(pthread_key_create)
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void*));//返回值:成功返回0;否则返回错误编号
- 功能:在分配线程私有数据之前,需要创建与私有数据关联的键。这个键用于获取对线程私有数据的访问,使用pthread_key_create可以创建一个键(参数1)
keyp参数:
要先定义一个pthread_key_t类型的键变量,然后将该键变量的地址赋值给此参数,之后pthread_key_create就可以初始化该键
这个键可以被进程中的所有线程使用,然后线程把这个键与自己线程内的私有数据地址进行关联
destructor参数:
此函数是与键关联的析构函数,函数的参数就是keyp(因为键与线程私有数据地址相关联(相同),所以传入的也就是私有数据的地址)
如果线程使用malloc等函数为线程私有数据分配内存,此参数作为析构函数就会释放线程私有数据分配的内存。如果线程在没有释放内存之前就退出,那么这么内存就会丢失(造成线程所属的进程出现了内存泄漏)
如果此参数为空,就表明没有析构函数与这个键关联
OK前面基本介绍这个函数的内部逻辑
就做了下面3件事情
- 初始化函数指针,比如malloc要调用哪个函数
- 构造一个MemoryTrace对象单例
- 创建一个线程私有数据__thread_internal_disabler_key
继续往下,可以看到做完init_no_alloc_allowed后,立马获取了上面第2步创建的单例对象,该对象调用了AllMonitoringIsDisabled()接口,判断是否执行后面的语句(通过代码名称可以猜测,在全局初始化之前需要进行检查)
该接口中使用到了前面第3步创建的线程私有数据
Linux线程私有数据Thread-specific Data(TSD) 详解 - 知乎
线程局部存储-pthread_getspecific和pthread_setspecific使用_tiny丶的博客-CSDN博客
通过上面博客讲解,大概明白,线程初始化内存检测之前,需要判断私有数据是否被设置过,像现在的情况,第一次执行,__monitoringDisabler初始值为0且没有调用pthread_setspecific为key绑定值,因此这个位置就会返回false,然后调用的地方继续往下走
这个判断是处理多线程调用的时候处理的
继续往下
第一次执行判断返回false,那就回执行MemoryTrace::init_full_from_once函数
其实就是调用下面函数
查看这个代码,主体逻辑都是在获取环境变量,判断是否设置了些参数,其实对于TSD还是有点没有理解透
这个位置创建一个线程私有数据的目的是什么?
(应该是在线程退出的时候,清理内存统计类的环境,这个地方创建了key,为什么不在这里给key绑定资源,而是在别的位置绑定?)
__monitoringDisabler这个参数不是原子变量,先++然后--,不会有多线程的问题吗
继续查看这个函数后续获取环境变量的代码,比如我们经常通过信号来开启和关闭内存泄漏检测,
LD_PRELOAD=/usr/lib/libleaktracer.so LEAKTRACER_ONSIG_STARTALLTHREAD=USR1 LEAKTRACER_ONSIG_REPORT=USR2 LEAKTRACER_ONSIG_REPORTFILENAME=leaks.out demo
看下它在做什么
这个里面涉及到信号捕获函数
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);◆ signum:要操作的信号。
◆ act :要设置的对信号的新处理方式。
◆ oldact :原来对信号的处理方式。
◆ 返回值:0 表示成功,-1 表示有错误发生struct sigaction 类型用来描述对信号的处理,定义如下:
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};◆ sa_handler 是一个函数指针
◆ sa_sigaction 则是另一个信号处理函数,它有三个参数,可以获得关于信号的更详细的信息(当 sa_flags 成员的值包含了 SA_SIGINFO 标志时,系统将使用 sa_sigaction 函数作为信号处理函数,否则使用 sa_handler 作为信号处理函数)
◆ sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生
◆ sa_flags 成员用于指定信号处理的行为,它可以是一下值的“按位或”组合(我们只学习SA_SIGINFO ,其他不去管)
◆ re_restorer 成员则是一个已经废弃的数据域,不要使用。
这个信号触发的函数sigactionHandler到底在做什么操作?
可以看到这里有3个判断,
第一个判断信号是不是触发内存检测开始检测
第二个判断信号是不是停止内存检测
第二个判断信号是不是现在输出报告
先看怎么触发的内存检测开始
这个位置为什么又调用了一次Setup?
后续的判断,猜测就是判断当前是不是已经停止了内存检测,如果当前仍然在内存检测过程中,这个时候再次触发开始检测,里面的代码不执行,如果当前处理非检测中,则将检测运行中标记设置为true
如果当前没有执行内存检测,那将上次的结果清理掉
TMapMemoryInfo是一个模板类,还挺复杂的,其实就是维护每次申请内存的信息
再往后这个stopMonitoringPerThreadAllocations()是干嘛的,停止所有线程检测?
这个操作内部就是将线程参数中监测内存分配的标志设置为false
上面是开始的处理方式
结束的处理方式:
上面其实就是将3个标志为全部设置为false
__monitoringAllThreads = false(如果有别的线程已经设置成了false,就将线程的检测分配标志设置为false)
__monitoringReleases = false
继续往后,init_full最后几步
这里有个堆栈输出,不知道为什么,感觉像是初始化堆栈输出逻辑一样,因为输出之后bt没有用到,看注释好像也是这个意思
初始化完成
二、退出
MemoryTraceOnExit
退出(这个只是将内存检测工具退出,并不是将主程序退出)需要使用环境变量设置才行,如果不设置该值,检测工具将会随着主程序一直工作(实际使用中没有必要设置该值)
看下这个接口做了什么
1. stopAllMonitoring
和前面停止所有线程检测一样调用这个接口
然后输出报告(这个比较重要,可能有的需要定制)
就是调用writeLeaksPrivate函数,函数的主体部分如下所有,就是遍历链表所有的节点信息,并输出到文件,包括申请的地址和大小等
第二节再详细的梳理这个链表中的数据结构是怎么样的