一个线程内存泄漏问题定位过程

一口Linux 2021-09-16 11:50

推荐关注👇下方公众号学习更多Linux、驱动知识!


记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获。

1. 初步定位

是否存在内存泄漏:想到内存泄漏,首先查看/proc/meminfo,通过/proc/meminfo可以看出总体内存在下降。确定内存泄漏确实存在。top中可以显示多种形式内存,进而可以判断是那种泄漏。比如vss/rss/pss等。

确定哪个进程内存泄漏:通过top即可查看到是哪个进程在泄漏。至此基本可以确定到哪个进程。

确定进程泄漏内存类型:然后查看进程的/proc/<pid>/maps,通过maps可以看出泄漏的内存类型(堆、栈、匿名内存等等),有时候运气好可以直接判断泄漏点。

如果是slab:可以通过/proc/slabinfo,可以看出进程的动态变化情况。如果确定是哪一个slab,那么可以在/sys/kernel/slab/<slab name>/alloc_calls和free_calls中直接找到调用点。当然看到的是内核空间的函数。

使用mcheck():可以检查malloc/free造成的泄漏问题,详细参考《2.
mtrace/muntrace/MALLOC_TRACE(重复释放、泄漏)》

通过如下脚本,然后对每次抓取内容进行Beyond Compare。每个一定周期抓取相关内存消耗信息。

#!/bin/bash
echo > mem_log.txt
while true
do
cat /proc/meminfo >>mem_log.txt
cat /proc/<pid>/maps >>mem_log.txt
cat /proc/slabinfo >>mem_log.txt
sleep 240
done

当然还有其他工具gcc Sanitier、Valgrind等等,由于嵌入式环境受限未能使用。

2. 深入定位

同步查看meminfo、maps、slabinfo,发觉进程虚拟内存损耗很快,远比系统MemFree损耗快。而且slabinfo没有和maps同步损耗。

所以问题重点检查maps问题。

00010000-00083000 r-xp 00000000 b3:11 22         /heop/package/AiApp/AiApp
00092000-00099000 rwxp 00072000 b3:11 22 /heop/package/AiApp/AiApp
00099000-00b25000 rwxp 00000000 00:00 0 [heap]
00b51000-00b52000 ---p 00000000 00:00 0
00b52000-01351000 rwxp 00000000 00:00 0 [stack:30451]
01351000-01352000 ---p 00000000 00:00 0
01352000-01b51000 rwxp 00000000 00:00 0
01b51000-01b52000 ---p 00000000 00:00 0
01b52000-02351000 rwxp 00000000 00:00 0 [stack:30432]
02351000-02352000 ---p 00000000 00:00 0
02352000-02b51000 rwxp 00000000 00:00 0
02b51000-02b52000 ---p 00000000 00:00 0
...
64f55000-65754000 rwxp 00000000 00:00 0 [stack:28646]
65754000-65755000 ---p 00000000 00:00 0
65755000-65f54000 rwxp 00000000 00:00 0 [stack:28645]
65f54000-65f55000 ---p 00000000 00:00 0
65f55000-66754000 rwxp 00000000 00:00 0 [stack:28642]
66754000-6675a000 r-xp 00000000 00:02 5000324 /usr/lib/AiApp/gstreamer-1.0/libgsticcsink.so
6675a000-66769000 ---p 00000000 00:00 0
...
6699f000-669a0000 rwxp 00000000 00:02 4999516 /usr/lib/AiApp/gstreamer-1.0/libgstapp.so
669a0000-66a2e000 rwxp 00000000 00:02 4999517 /usr/lib/AiApp/gstreamer-1.0/libgstlive555src.so
66a2e000-66a3e000 ---p 00000000 00:00 0
66a3e000-66a44000 rwxp 0008e000 00:02 4999517 /usr/lib/AiApp/gstreamer-1.0/libgstlive555src.so
66a44000-66a45000 rwxp 00000000 00:00 0
66a45000-66a46000 ---p 00000000 00:00 0
66a46000-67245000 rwxp 00000000 00:00 0 [stack:28631]
67245000-67246000 ---p 00000000 00:00 0
67246000-67a45000 rwxp 00000000 00:00 0 [stack:28630]
...
6b245000-6b246000 ---p 00000000 00:00 0
6b246000-6ba45000 rwxp 00000000 00:00 0 [stack:28613]
6ba45000-6ba46000 ---p 00000000 00:00 0
6ba46000-6c245000 rwxp 00000000 00:00 0 [stack:28610]
6c245000-71066000 rwxs 00000000 00:01 196614 /SYSV5553fc99 (deleted)
71066000-71067000 ---p 00000000 00:00 0
71067000-71866000 rwxp 00000000 00:00 0 [stack:28609]
71866000-71867000 ---p 00000000 00:00 0
71867000-72066000 rwxp 00000000 00:00 0 [stack:28608]
72066000-72228000 rwxs e3dc4000 00:02 6918 /dev/mmz_userdev
72228000-725ac000 rwxs e3a40000 00:02 6918 /dev/mmz_userdev
725ac000-75cac000 rwxs 00000000 00:01 131076 /SYSV6702121c (deleted)
75cac000-75e8a000 rwxs 00000000 00:01 98307 /SYSV6602121c (deleted)
75e8a000-7608e000 rwxp 00000000 00:00 0...
76eeb000-76efb000 ---p 00000000 00:00 0
76efb000-76eff000 r-xp 000ce000 00:02 1234 /lib/libstdc++.so.6.0.20
76eff000-76f01000 rwxp 000d2000 00:02 1234 /lib/libstdc++.so.6.0.20
76f01000-76f08000 rwxp 00000000 00:00 0
76f08000-76f0f000 r-xp 00000000 00:02 1235 /lib/ld-uClibc-0.9.33.2.so
76f1a000-76f1e000 rwxp 00000000 00:00 0
76f1e000-76f1f000 rwxp 00006000 00:02 1235 /lib/ld-uClibc-0.9.33.2.so
76f1f000-76f20000 ---p 00000000 00:00 0...
7c720000-7cf1f000 rwxp 00000000 00:00 0 [stack:30574]
7cf1f000-7cf20000 ---p 00000000 00:00 0
7cf20000-7e121000 rwxp 00000000 00:00 0 [stack:30575]
7eef7000-7ef18000 rwxp 00000000 00:00 0 [stack]
7efb7000-7efb8000 r-xp 00000000 00:00 0 [sigpage]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]


通过多次maps对比,可以发现[stack:TID]类型的内存以及一个匿名内存在不停增加消耗内存。

其中[stack:TID]类型的内存,在内核查找相关代码没有明确对应属性。初步判断是线程的栈,TID表示线程id号。

所以这里应该是某个线程泄漏。

2.1 线程栈泄漏(Joinable线程栈)

一个导致线程栈泄漏原因可能是对于一个Joinable线程,系统会创建线程私有的栈、threand ID、线程结束状态等信息。

如果此线程没有pthread_join(),那么系统不会对以上信息进行回收。这就可能造成线程栈等泄漏。

确定线程栈泄漏的方法是:通过ls /proc/<pid>/task | wc -l确定进程下线程数目。然后在maps中检查[stack:TID]数目。两者如果不一致,则存在Joinable线程没有调用pthread_join()造成的泄漏。

如果maps没有[stack:TID],可以通过pmap <pid> | grep <stack size> | wc -l,即通过检查栈大小的vma数目来确定栈数目。

关于线程内存泄漏参考:《Avoiding memory leaks in POSIX thread programming》

3. 问题根源

通过检查线程栈消耗与实际线程数目,发现两者数目吻合。所以线程并没有退出。也即不是由于未使用pthread_join()导致的内存泄漏。

然后根据maps中[stack:TID]的pid号,cat /proc/<pid>/comm发现是同一个线程不停创建。但是没有释放。

其实通过top -H -p <pid>和maps也可发现问题,中间走了弯路。

所以问题的根源是,进程不停创建但是没有退出造成内存消耗殆尽

4. 收获

有两个收获,一是创建的pthread线程Join和Detach两种状态下内存处理差别;

二是在进程maps中显示线程栈[stack:TID]更有利于调试。

4.1 pthread线程的join和detach区别

《Avoiding memory leaks in POSIX thread programming》讲到如何避免POSIX线程编程时内存泄漏。

https://developer.ibm.com/tutorials/l-memory-leaks/

首先pthread_create()创建的线程默认是joinable的。

对于joinable线程,系统会分配私有内存存储线程结束状态、线程栈、线程ID等等资源。这些资源会一直存在,直到线程结束并且线程被其他线程joined。

所以确保joinable线程资源得到释放的两个条件是:线程退出、被其他线程joined

对于detached线程,如果其退出,那么系统会自动回收其占用的资源。

关于joinable线程没有被其他线程joined造成内存泄漏的实验。

#include<stdio.h>
#include<pthread.h>

void run() {
pthread_exit(0);
}

int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
usleep(10);
count++;
}
return 0;
}


输出结果如下:

ERROR, rc is 11, so far 32751 threads created
Fail:: Cannot allocate memory

总共创建了32571个线程,造成内存消耗殆尽。

通过对比中间过程的maps,可以发现每次增加一个8MB的栈以及一个分隔页。


在pthread_create()之后增加pthread_join()则内存非常稳定。

#include<stdio.h>
#include<pthread.h>

void run() {
pthread_exit(0);
}

int main () {
pthread_t thread;
int rc;
long count = 0;
while(1) {
if(rc = pthread_create(&thread, 0, run, 0) ) {
printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
perror("Fail:");
return -1;
}
pthread_join(thread, NULL);
usleep(10);
count++;
}
return 0;
}


借用文档里面一句话总结一下:

Joinable threads should be joined during programming.

If you are creating joinable threads in your program, don’t forget to call pthread_join(pthread_t, void**) to recycle the private storage allocated to the thread.

调用pthread_join()将阻塞线程自己,一直等到加入的线程运行结束。

线程可以分为两种:joined和detached。并不是所有线程创建后都默认joinable,需要显式指定属性。

joinable线程在创建后,可以通过pthread_detach()显式分离。在分离后,不可以再合并。

如果一个线程结束运行,但没有被join。则它的状态类似进程中的Zombie Process,即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join()来等待线程结束,并可得到线程的退出代码,回收其资源。

如果父进程调用pthread_detach(child_thread_id)或者子进程调用pthread_detack(pthread_self())即可将子进程状态设置为detached,该程序运行结束后会自动释放所有资源。

4.2 关于在maps中显示[stack:TID]

在进程maps中显示线程栈信息,最后在内核中被放弃。

首先在《procfs: mark thread stack correctly in proc/<pid>/maps》中,添加了[stack:TID]用于表示此vma对应的是线程TID的stack区域。

这样做的好处是,可以从maps中明确知道此段vma是被哪个线程使用的。

有一个坏处就是先线程非常多情况下,主线程中为了显示[stack:TIS],开销就会很大,而实际上用处不是很大。

所以在《proc: revert /proc/<pid>/maps [stack:TID] annotation》将进程maps中的[stack:TID]删除了,只显示为匿名内存。

Detecting leaks

Just as in other memory leaks, the problem may not be obvious when the process is started. So here’s a way to detect such problems without needing to access source code:

Count the number of thread stacks in the process. That includes the number of running active threads and terminated threads.

Count the number of active running threads in the process.

Compare the two. If the number of the existing thread stacks is greater than the number of active running threads, and the dispersion of these two numbers keeps increasing as the program continues running, then memory is leaking.

Use /proc/PID/task to count active threads
[root@oceanbase task]# ls /proc/29363/task | wc -l
555
Use pmap to count thread stacks
cat /proc/29363/maps |grep stack |wc -l
555

----- 1 root root 64 Aug 14 17:19 1032 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1031 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1030 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1029 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1028 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1027 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1026 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1025 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1024 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1023 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1022 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1021 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1020 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1019 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1018 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1017 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1016 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1015 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1014 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1013 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1012 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1011 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1010 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1009 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1008 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1007 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1006 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1005 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1004 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1003 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1002 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1001 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1000 -> anon_inode:[eventpoll]
[root@oceanbase fd]#


最终再《fs/proc: Stop trying to report thread stacks》将所有[stack:TID]全部移除。

那么在没有[stack:TID]的情况下如何断定vma是否是线程栈呢?

首先线程栈大小可以通过ulimit -s查看,所以maps中vma大小和这个一致;并且属性应该是匿名的rw-p。

然后上面应该是一页大小作为分隔区间,分隔页的属性应该是---p。

- END -


关注,回复【1024】海量Linux资料赠送

 精彩文章合集

linux入门
C语言
Linux驱动
ARM
计算机网络
粉丝问答
所有原创
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看
一口Linux 写点代码,写点人生!
评论 (0)
  •        当今社会已经步入了知识经济的时代,信息大爆炸,新鲜事物层出不穷,科技发展更是一日千里。知识经济时代以知识为核心生产要素,通过创新驱动和人力资本的高效运转推动社会经济发展。知识产权(IP)应运而生,成为了知识经济时代竞争的核心要素,知识产权(Intellectual Property,IP)是指法律赋予人们对‌智力创造成果和商业标识等无形财产‌所享有的专有权利。其核心目的是通过保护创新和创意,激励技术进步、文化繁荣和公平竞争,同时平衡公共利益与
    广州铁金刚 2025-03-24 10:46 65浏览
  • 在人工智能与物联网技术蓬勃发展的今天,语音交互已成为智能设备的重要功能。广州唯创电子推出的WT3000T8语音合成芯片凭借其高性能、低功耗和灵活的控制方式,广泛应用于智能家居、工业设备、公共服务终端等领域。本文将从功能特点、调用方法及实际应用场景入手,深入解析这款芯片的核心技术。一、WT3000T8芯片的核心功能WT3000T8是一款基于UART通信的语音合成芯片,支持中文、英文及多语种混合文本的实时合成。其核心优势包括:高兼容性:支持GB2312/GBK/BIG5/UNICODE编码,适应不同
    广州唯创电子 2025-03-24 08:42 154浏览
  • 在智能终端设备快速普及的当下,语音交互已成为提升用户体验的关键功能。广州唯创电子推出的WT3000T8语音合成芯片,凭借其卓越的语音处理能力、灵活的控制模式及超低功耗设计,成为工业控制、商业终端、公共服务等领域的理想选择。本文将从技术特性、场景适配及成本优势三方面,解析其如何助力行业智能化转型。一、核心技术优势:精准、稳定、易集成1. 高品质语音输出,适配复杂环境音频性能:支持8kbps~320kbps宽范围比特率,兼容MP3/WAV格式,音质清晰自然,无机械感。大容量存储:内置Flash最大支
    广州唯创电子 2025-03-24 09:08 185浏览
  • WT588F02B是广州唯创电子推出的一款高性能语音芯片,广泛应用于智能家电、安防设备、玩具等领域。然而,在实际开发中,用户可能会遇到烧录失败的问题,导致项目进度受阻。本文将从下载连线、文件容量、线路长度三大核心因素出发,深入分析烧录失败的原因并提供系统化的解决方案。一、检查下载器与芯片的物理连接问题表现烧录时提示"连接超时"或"设备未响应",或烧录进度条卡顿后报错。原因解析接口错位:WT588F02B采用SPI/UART双模通信,若下载器引脚定义与芯片引脚未严格对应(如TXD/RXD交叉错误)
    广州唯创电子 2025-03-26 09:05 74浏览
  • 在智能终端设备开发中,语音芯片与功放电路的配合直接影响音质表现。广州唯创电子的WTN6、WT588F等系列芯片虽功能强大,但若硬件设计不当,可能导致输出声音模糊、杂音明显。本文将以WTN6与WT588F系列为例,解析音质劣化的常见原因及解决方法,帮助开发者实现清晰纯净的语音输出。一、声音不清晰的典型表现与核心原因当语音芯片输出的音频信号存在以下问题时,需针对性排查:背景杂音:持续的“沙沙”声或高频啸叫,通常由信号干扰或滤波不足导致。语音失真:声音断断续续或含混不清,可能与信号幅度不匹配或功放参数
    广州唯创电子 2025-03-25 09:32 60浏览
  • 文/Leon编辑/cc孙聪颖‍“无AI,不家电”的浪潮,正在席卷整个家电行业。中国家电及消费电子博览会(AWE2025)期间,几乎所有的企业,都展出了搭载最新AI大模型的产品,从电视、洗衣机、冰箱等黑白电,到扫地机器人、双足机器人,AI渗透率之高令人惊喜。此番景象,不仅让人思考:AI对于家电的真正意义是什么,具体体现在哪些方面?作为全球家电巨头,海信给出了颇有大智慧的答案:AI化繁为简,将复杂留给技术、把简单还给生活,是海信对于AI 家电的终极答案。在AWE上,海信发布了一系列世俱杯新品,发力家
    华尔街科技眼 2025-03-23 20:46 72浏览
  • 在智慧城市领域中,当一个智慧路灯项目因信号盲区而被迫增设数百个网关时,当一个传感器网络因入网设备数量爆增而导致系统通信失效时,当一个智慧交通系统因基站故障而导致交通瘫痪时,星型网络拓扑与蜂窝网络拓扑在构建广覆盖与高节点数物联网网络时的局限性便愈发凸显,行业内亟需一种更高效、可靠与稳定的组网技术以满足构建智慧城市海量IoT网络节点的需求。星型网络的无线信号覆盖范围高度依赖网关的部署密度,同时单一网关的承载设备数量有限,难以支撑海量IoT网络节点的城市物联系统;而蜂窝网络的无线信号覆盖范围同样高度依
    华普微HOPERF 2025-03-24 17:00 174浏览
  •       知识产权保护对工程师的双向影响      正向的激励,保护了工程师的创新成果与权益,给企业带来了知识产权方面的收益,企业的创新和发明大都是工程师的劳动成果,他们的职务发明应当受到奖励和保护,是企业发展的重要源泉。专利同时也成了工程师职称评定的指标之一,专利体现了工程师的创新能力,在求职、竞聘技术岗位或参与重大项目时,专利证书能显著增强个人竞争力。专利将工程师的创意转化为受法律保护的“无形资产”,避免技术成果被他人抄袭或无偿使
    广州铁金刚 2025-03-25 11:48 124浏览
  • 在嵌入式语音系统的开发过程中,广州唯创电子推出的WT588系列语音芯片凭借其优异的音质表现和灵活的编程特性,广泛应用于智能终端、工业控制、消费电子等领域。作为该系列芯片的关键状态指示信号,BUSY引脚的设计处理直接影响着系统交互的可靠性和功能拓展性。本文将从电路原理、应用场景、设计策略三个维度,深入解析BUSY引脚的技术特性及其工程实践要点。一、BUSY引脚工作原理与信号特性1.1 电气参数电平标准:输出3.3V TTL电平(与VDD同源)驱动能力:典型值±8mA(可直接驱动LED)响应延迟:语
    广州唯创电子 2025-03-26 09:26 72浏览
  • 核心板简介创龙科技 SOM-TL3562 是一款基于瑞芯微 RK3562J/RK3562 处理器设计的四核 ARM C ortex-A53 + 单核 ARM Cortex-M0 全国产工业核心板,主频高达 2.0GHz。核心板 CPU、R OM、RAM、电源、晶振等所有元器件均采用国产工业级方案,国产化率 100%。核心板通过 LCC 邮票孔 + LGA 封装连接方式引出 MAC、GMAC、PCIe 2.1、USB3.0、 CAN、UART、SPI、MIPI CSI、MIPI
    Tronlong 2025-03-24 09:59 181浏览
我要评论
0
2
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦