0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

线程内存泄漏问题的定位

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-13 11:38 次阅读

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

1. 初步定位

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

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

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

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

使用mcheck():可以检查malloc/free造成的泄漏问题。

通过如下脚本,然后对每次抓取内容进行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//task | wc -l确定进程下线程数目。然后在maps中检查[stack:TID]数目。两者如果不一致,则存在Joinable线程没有调用pthread_join()造成的泄漏。

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

3. 问题根源

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

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

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

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

4. 收获

有两个收获,一是创建的pthread线程Join和Detach两种状态下内存处理差别;二是在进程maps中显示线程栈[stack:TID]更有利于调试。

4.1 pthread线程的join和detach区别

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

首先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 createdn", 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 createdn", 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//maps》中,添加了[stack:TID]用于表示此vma对应的是线程TID的stack区域。

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

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

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

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

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

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

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

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 嵌入式
    +关注

    关注

    5082

    文章

    19096

    浏览量

    304618
  • 函数
    +关注

    关注

    3

    文章

    4326

    浏览量

    62555
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19674
  • 内存泄漏
    +关注

    关注

    0

    文章

    39

    浏览量

    9211
收藏 人收藏

    评论

    相关推荐

    Linux上对进程进行内存分析和内存泄漏定位

    和redis进程。当memfree不够时,内核会通过回写机制(pdflush线程)把cached和buffered内存回写到后备存储器,也可以通过手动方式显式释放cache内存 echo 3 >
    发表于 07-09 08:15

    AliOS Things 维测典型案例分析 —— 内存泄漏

    使用情况全部输出,方便定位)这是由于内存不足,无法从系统内存池中mallo出动态内存,出现这种现象一般有2种原因:某组件在运行中持续分配了较大内存
    发表于 10-17 11:29

    Executors使用不当引起的内存泄漏怎么解决

    是否知道了此次引起内存泄漏的原因,其实就是因为阻塞队列的容量过大。  如果不手动的指定阻塞队列的大小,那么它默认是Integer.MAX_VALUE,我们的线程池只有20个线程可以处理
    发表于 12-23 17:38

    内存泄漏定位该如何去实现呢

    嵌入式之内存泄漏定位篇在嵌入式开发中,经常会使用malloc,free分配释放堆内存,当malloc,free不配对使用时,就会导致内存一点
    发表于 12-17 07:24

    写了一个内存泄漏检查工具

    的malloc或者free的调用,记录申请内存的大小,地址,和调用的函数。以便追踪内存泄漏。并且开启一个线程,每隔一段时间监测是否有哪个函数申请的
    发表于 12-17 08:25

    分享一种内存泄漏定位排查技巧

    这里写自定义目录标题1.对malloc,free进行封装2.如何确定MALLOC_SIZE_OFFSET大小(编译器malloc长度地址偏移)3.如何监测内存有无泄漏4.如何快速定位内存泄漏
    发表于 12-17 08:13

    如何编译使用内存泄漏定位工具

    1.我们知道有个内存泄漏定位工具: valgrind, 非常优秀。现在已经支持arm版本,下面看看如何编译使用:2.下载源码:
    发表于 12-17 08:13

    sqlite软件包内存泄漏如何解决?

    at function:rt_object_init, line number:358, 而从github下载的就能运行),运行一段时间后发现使用内存不断增大,用memtrace工具确定是操作数据库的线程导致,请问造成
    发表于 05-24 15:25

    解析Web内存分析与内存泄漏定位

    JavaScript 中开发者并不需要手动地为对象申请内存,只需要声明变量,JavaScript Runtime 即可以自动地分配内存.所谓的内存泄漏,即是指某个对象被无意间添加了某条
    发表于 11-10 15:00 2446次阅读

    ThreadLocal发生内存泄漏的原因

    ,就可能会导致内存泄漏。下面,我们将围绕三个方面来分析 ThreadLocal 内存泄漏的问题 ThreadLocal 实现原理 ThreadLocal为什么会
    的头像 发表于 05-05 16:23 3672次阅读

    内存泄漏的特点和类型

    在计算机科学中,内存泄漏(memory leak)指由于疏忽或错误使程序未能释放而造成不能再使用的内存的情况。内存泄漏并非指
    的头像 发表于 06-20 10:58 2813次阅读

    内存泄漏问题原理及检视方法

    可能不少开发者都遇到过内存泄漏导致的网上问题,具体表现为单板在现网运行数月以后,因为内存耗尽而导致单板复位现象。一方面,内存泄漏问题属于比较
    的头像 发表于 10-10 10:42 2535次阅读

    什么是内存泄漏内存泄漏有哪些现象

    内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题,甚至 Windows 与 Linux 这类系统软件也或多或少存在着内存泄漏
    的头像 发表于 09-05 17:24 9668次阅读

    什么是内存泄漏?如何避免JavaScript内存泄漏

    JavaScript 代码中常见的内存泄漏的常见来源: 研究内存泄漏问题就相当于寻找符合垃圾回收机制的编程方式,有效避免对象引用的问题。
    发表于 10-27 11:30 396次阅读
    什么是<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>?如何避免JavaScript<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>

    C语言内存泄漏问题原理

    内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈
    发表于 03-19 11:38 518次阅读
    C语言<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>问题原理