一、简介:
SQUID自身使用了一种简单的内存调试方式,可以在程序运行的时候对内存的使用情况进行监控调试(指开发层面的监控,如泄漏,内存合法性,不是运维层面的内存监控)。本文简单介绍其内部实现。不过就其内存泄露检查部分,仍有部分疑问。。
二、主要数据结构:
xmalloc开辟了几个简单的哈希桶,用于记录每次分配的内存的大小,分配的时机(所在函数,所在函数所在行号,所在文件名),用分配的指针进行一定的移位作为key。
static void *(*malloc_ptrs)[DBG_ARRY_SZ]; static int malloc_size[DBG_ARRY_BKTS][DBG_ARRY_SZ]; static char *malloc_file[DBG_ARRY_BKTS][DBG_ARRY_SZ]; static short malloc_line[DBG_ARRY_BKTS][DBG_ARRY_SZ]; static int malloc_count[DBG_ARRY_BKTS][DBG_ARRY_SZ];
同时有几个临时变量,用来记录当前函数名、文件名、行号。
char *xmalloc_file = ""; int xmalloc_line = 0; char *xmalloc_func = "";三、主要流程:
3.1 分配与释放:
内存分配时,通过宏将函数名、文件名、行号赋值给全局变量,后续的记录都依赖于这个全局变量(其实还可以通过传参的方式,如SQUID中对cbdata_debug的管理那样)。
#define xmalloc(size) (xmalloc_func="xmalloc",xmalloc_line=__LINE__,xmalloc_file=__FILE__,xmalloc(size))
从底层获取完内存后,会对分配的内存和大小进行检查。根据指针的23位、45位、67位的值的和取两位作为key(0x12345670则(23+45+67) & 0xff)值找到所在的桶,在该桶内对所有已有记录的指针进行判断,判断与当前内存是否有重叠。
B = DBG_HASH_BUCKET(p); for (I = 0; I < DBG_ARRY_SZ; I++) { if (!(P = malloc_ptrs[B][I])) continue; /* 获取P的范围. */ Q = P + malloc_size[B][I]; if (P <= p && p < Q) { do_something(); } }
然后在桶中选择一个位置,把当前分配的内存的信息记录在案。
for (I = 0; I < DBG_ARRY_SZ; I++) { if (malloc_ptrs[B][I]) continue; malloc_ptrs[B][I] = p; malloc_size[B][I] = (int) sz; malloc_file[B][I] = xmalloc_file; malloc_line[B][I] = xmalloc_line; malloc_count[B][I] = xmalloc_count; break; }
在函数xmalloc_show_trace中,会把相应的分配进行统计,统计总共分配的内存大小和总共分配的内存次数。malloc时候sign为1,free时候sign为-1(realloc的时候先释放后分配)。
sz = xmallocblksize(p) * sign; xmalloc_total += sz; xmalloc_count += sign > 0;
内存释放时候则相反,直接把桶中对应的元素删除,相应位置置位。
3.2 内存泄露检查:
这一部分有较大疑问,在打开相应选项以后,使用xmalloc_find_leaks会导致程序core。对其其中几个细节有部分疑问。
先简单介绍下内存泄露代码的理解。
内存泄露的检查调用点在SQUID shutdown的末尾,可以对本次服务运行的整个过程进行一个分析。
xmalloc_find_leaks是内存泄露检查的入口,这个函数由两个部分组成。
xmalloc_scan_region(&_etext, (void *) sbrk(0) - (void *) &_etext, 0); for (B = 0; B < DBG_ARRY_BKTS; B++) { for (I = 0; I < DBG_ARRY_SZ; I++) { if (malloc_ptrs[B][I] && malloc_refs[B][I] == 0) { /* Found a leak... */ fprintf(stderr, "Leak found: %p", malloc_ptrs[B][I]); fprintf(stderr, " %s", malloc_file[B][I]); fprintf(stderr, ":%d", malloc_line[B][I]); fprintf(stderr, " size %d", malloc_size[B][I]); fprintf(stderr, " allocation %d\n", malloc_count[B][I]); leak_sum += malloc_size[B][I]; } } }
第一部分是xmalloc_scan_region,起始地址为&_etext,即代码段的结尾,扫描的范围为&_etext到sbrk(0),即整个代码段到数据段的范围,但是注意,内存分配的时候是有两种分配方式,一种是brk分配,这种分配方式会扩大数据段,另一种分配方式是直接映射,在数据段之上,栈段之下,这一部分应该是在sbrk(0)之外,因此个人理解,这次的扫描扫描的是整个brk分配的小内存。
while (ptr <= end) { <span style="white-space:pre"> </span>void *p = *(void **) ptr; if (p && p != start) { B = DBG_HASH_BUCKET(p); for (I = 0; I < DBG_ARRY_SZ; I++) { if (malloc_ptrs[B][I] == p) { if (!malloc_refs[B][I]++) { /* A new reference */ fprintf(stderr, "%*s%p %s:%d size %d allocation %d\n", <span style="white-space:pre"> </span>depth, "", <span style="white-space:pre"> </span> malloc_ptrs[B][I], malloc_file[B][I], malloc_line[B][I], malloc_size[B][I], malloc_count[B][I]); sum += malloc_size[B][I]; xmalloc_scan_region(malloc_ptrs[B][I], malloc_size[B][I], depth + 1); if (depth == 0) { if (sum != malloc_size[B][I]) fprintf(stderr, "=== %d bytes\n", sum); <span style="white-space:pre"> </span>sum = 0; } } } } } ptr += XMALLOC_LEAK_ALIGN; }这部分的遍历有些不理解,水平有限,还请懂的人指教:
1. void *p = *(void **) ptr; 我理解应该是搜索整个数据段的所有地址,查看这个地址是否在桶中有记录,如果有记录该点是泄露。但是这个取ptr地址中的内容不太明白是为什么。
2. 其实从text段结束并不是马上就是数据段,直接取地址会导致有不少数据是无法访问的。
3. 每次搜索到就标记,但是没有统计到函数find_leak中的leak_sum中,是否数据段中的内容不计算在泄露中?
4. 其实直接判断桶中仍然有记录的值,就是malloc和free不成对出现的指针,就已经是泄露的内容,为什么用这么复杂的方式来统计?
5. 偏移大小4,好像和实际地址对齐不太一致?
第二部分是检查没有扫描到的,但是没有正常释放的内存,个人理解这次遍历这个桶,是为了找出因为mmap而泄露的内存。
4. 总结:
虽然对内存泄漏检查仍有疑惑,但是对于其对内存调试的方法是值得学习的,在SQUID中的cbdata管理采用的也是类似方式。