pthread TLS析构函数释放内存未被valgrind/massif检测到问题
问题现象
创建带自定义析构函数的TLS键后,启动大量线程并将该TLS键传递给每个线程使用:每个线程自行分配堆内存,将内存指针存入TLS槽位,约定由TLS析构函数负责释放该段内存,且应用退出前会通过pthread_join等待所有线程执行完成。
但使用valgrind/massif运行该应用时,工具报告上述TLS中存储的内存未被释放。
复现代码
主函数逻辑:
int main(int argc, char **argv) { pthread_key_t* key = new pthread_key_t(); pthread_key_create(key, my_destructor); pthread_t threads[32000]; for(int i=0; i<32000; ++i) pthread_create(&threads[i], NULL, my_thread, key); for(int i=0; i<32000; ++i) pthread_join(threads[i], NULL); return 0; }
线程执行函数(分配内存并存入TLS):
extern "C" void* my_thread(void* p) { pthread_setspecific(*(pthread_key_t*)p, malloc(100)); return NULL; }
TLS析构函数(释放内存):
extern "C" void my_destructor(void *p) { free(p); }
运行环境与参数
- 系统环境:
Red Hat Enterprise Linux Server release 7.9 (Maipo) - 编译器:
gcc 4.9.4 - valgrind版本:3.19
- valgrind启动参数:
--tool=massif --heap=yes --pages-as-heap=yes --log-file=/tmp/my.log --massif-out-file=/tmp/my.massif.log
通过ms_print /tmp/my.massif.log分析输出文件,得到如下类似内存占用报告:
| ->01.75% (67,108,864B) 0x76F92D0: new_heap (in /usr/lib64/libc-2.17.so) | | ->01.75% (67,108,864B) 0x76F98D3: arena_get2.isra.3 (in /usr/lib64/libc-2.17.so) | | ->01.75% (67,108,864B) 0x76FF77D: malloc (in /usr/lib64/libc-2.17.so) | | ->01.75% (67,108,864B) 0x410300: my_thread (threadsT.cpp:136) | | ... | | <skipped by author> | | ... | | | ->00.00% (73,728B) in 1+ places, all below ms_print's threshold (01.00%)
已确认事实
通过在析构函数中添加插桩日志,已经验证:
- TLS析构函数确实被正常调用
- 析构函数按照预期逻辑完成了对应内存块的释放操作
问题解答
你的业务代码不存在实际的内存泄漏,这个报告是统计口径和glibc内存分配器缓存机制共同导致的统计偏差,不是真泄漏,也不是valgrind识别不到TLS析构函数里的free操作。
具体原因如下:
- 你启动时加了
--pages-as-heap=yes参数,这个参数下massif不会只统计应用层申请后未释放的内存,而是会把进程地址空间内所有通过mmap映射、属于堆范畴的内存页全部算成已占用内存。 - RHEL7.9自带的glibc 2.17使用ptmalloc2内存分配器,默认会给每个线程创建独立的内存分配arena,每个arena会通过mmap申请大块的内存页(你看到的67108864B刚好是64MB,就是这类arena的典型块大小)。当线程退出时,ptmalloc不会把这些arena占用的内存页通过munmap归还给操作系统,而是会把arena放到内部的空闲链表中缓存,留给后续新建的线程复用。
- 你调用
free释放的那100字节,确实已经被标记为空闲、可以被后续分配复用,但这部分内存所在的arena整块都还被ptmalloc持有、没有还给OS,所以开了--pages-as-heap=yes的massif会把这整块64MB的内存都算成已占用,回溯栈自然会落到你最初调用malloc的位置。
验证方法非常简单:把启动参数里的--pages-as-heap=yes去掉,再重新跑一次massif,你会看到这64MB的统计项直接消失——默认模式下massif只会统计应用层未调用free的内存,不会把分配器缓存的空闲页算成泄漏。
另外提两个代码里的小瑕疵,和你看到的64MB报告无关,但不符合规范:
- 你在main里用
new创建的pthread_key_t对象从来没有调用delete释放,会产生8字节的极小内存泄漏 - 所有线程执行完成后,你没有调用
pthread_key_delete销毁创建的TLS键,虽然进程退出时OS会自动回收所有资源,但属于不严谨的写法
内容的提问来源于stack exchange,提问作者nsh
相关产品推荐
相关产品推荐

