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

一口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 写点代码,写点人生!
评论
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 186浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 418浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 616浏览
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 58浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 143浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 113浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 117浏览
  • Ubuntu20.04默认情况下为root账号自动登录,本文介绍如何取消root账号自动登录,改为通过输入账号密码登录,使用触觉智能EVB3568鸿蒙开发板演示,搭载瑞芯微RK3568,四核A55处理器,主频2.0Ghz,1T算力NPU;支持OpenHarmony5.0及Linux、Android等操作系统,接口丰富,开发评估快人一步!添加新账号1、使用adduser命令来添加新用户,用户名以industio为例,系统会提示设置密码以及其他信息,您可以根据需要填写或跳过,命令如下:root@id
    Industio_触觉智能 2025-01-17 14:14 140浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 293浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 150浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦