请选择 进入手机版 | 继续访问电脑版
MSIPO技术圈 首页 IT技术 查看内容

LeakTracer代码学习(1)

2023-07-13

项目中有的时候会产生内存泄漏,以往的经验,检测工具更倾向于使用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_INITonce_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件事情

  1. 初始化函数指针,比如malloc要调用哪个函数
  2. 构造一个MemoryTrace对象单例
  3. 创建一个线程私有数据__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函数,函数的主体部分如下所有,就是遍历链表所有的节点信息,并输出到文件,包括申请的地址和大小等

第二节再详细的梳理这个链表中的数据结构是怎么样的

相关阅读

热门文章

    手机版|MSIPO技术圈 皖ICP备19022944号-2

    Copyright © 2024, msipo.com

    返回顶部