memleak
在日常的开发中,我们经常会遇到内存泄漏这样的老大难问题。随着eBPF的发展,我们可以使用eBPF来进行内存泄露的检测。在bcc中,就自带了一个memleak工具来帮助我们进行c/c++的内存泄漏检测。
memleak原理简单解析
memleak的思路比较明确:利用插桩统计分配的字节和释放的字节,两者进行抵消,从而寻找没有释放的、可能是泄露的调用栈。
以malloc分配为例,当malloc申请时:
malloc申请
会记录一个<pid, size>对,记录下哪个进程申请了多少内存;接着当分配完成以后:
malloc申请完成
会记录下<address, info>对,记录下虚拟地址以及相关的调用栈等信息;当我们调用free释放对应地址的时候:
free
我们就基于address来进行抵消,那么剩下的没有抵消的部分就可能是泄露的地方:
泄露
消失的八字节
在了解了memleak的原理后,我们发现有这样的一个patch:Fix data race on --combined-only(github.com/iovisor/bcc…
测试代码
可以看到,这里调用malloc分配了800000个字节,但是在修复完race condition后,结果却和我们想的不一样:
缺少8字节
竟然少了8个字节。笔者尝试将分配的字节从800000变成80,发现也会缺少八个字节,可见这不是一个线性变化的事情。那问题出在哪里呢?
追踪
为了便捷说明,这里将分配次数调整成了10次,一共分配80字节。
为了进行调试,我们尝试打印pid等信息,并进行追踪,重点观察分配4字节的部分:
trace打印追踪
好像找到一点线索了,这里同一个tid=2600823怎么会连续分配两次呢?我们尝试加了更多的打印信息,来看看是谁调用了gen_alloc_enter:
追踪调用者
可以看到,这里在由malloc第一次调用gen_alloc_enter之后,mmap也调用了gen_alloc_enter,由于我们的BPF_HASH(sizes, u32, u64)的主键是tid,所以原来为四字节的部分就被冲刷掉了,自然而然就少统计了四个字节。
理想中的统计抵消过程如下图(:表示map的映射):
理想的抵消
理想的统计泄露过程如下图(:表示map的映射):
理想的泄露发现过程
实际的统计泄露过程(:表示map的映射):
实际的情况
那么为什么会覆盖呢?这是因为malloc在进行内存分配的时候,如果内存分配器自身管理的内存也不够,就会通过mmap或者其他的方式来申请内存,所以会出现malloc调用后mmap调用的情况。
修复
现在我们已经知道了问题所在,就是map的key冲突导致的,那么我们怎么解决这个问题呢?
二级Map
笔者本来想尝试通过BPF_ARRAY_OF_MAPS或者BPF_HASH_OF_MAPS来实现二级map,一级map的key就是入口任务类别,二级map才是真正的存储,如图所示:
二级Map实现
但是笔者查阅资料以后发现,想要实现动态的二级Map有点困难,不能动态的设置Array或者一级Map的大小,所以暂时放弃了这种写法。
如果有了解的读者可以私信或者留言指导下。
新的key定义
由于第一种方式行不动,我们只能寻找第二种方式。既然问题的因为key冲突导致的,而key是进程的pid,类型是u32,比较容易冲突,那我们改变key的定义,让它不冲突就可以了。于是笔者尝试将key设置成如下的值:
利用高32位来作为区分位,这样,整个的泄露检测过程就变成了:
高32位区分
修复效果检验
完成以后,我们尝试重新运行一下:
修复成功
可以看到已成功修复。
总结
本文主要介绍了memleak的简单原理和对其的修复过程,该pr已经合入到bcc仓库中,详见tools/memleak: Fix the data error caused by the same key in map(github.com/iovisor/bcc…](github.com/AshinZ/perf…