Linux C/C++ 程序内存泄露排查

前言

由于C/C++程序可以动态申请内存,动态申请的内存位于程序的队区,如果程序比较复杂,程序员在编写代码的时候不小心,可能会存在申请了内存没有释放的情况,程序长期运行,会导致系统中用户程序可分配堆内存越来越少的,最终程序OOM崩溃。

Linux系统内存泄露检查

系统内存监控

/proc/meminfo 文件保存了系统内存使用情况,

MemTotal:       498364224 kB
MemFree:        341371712 kB
MemAvailable:   431430528 kB
Buffers:            3456 kB
Cached:         89801856 kB
SwapCached:            0 kB
Active:         36263872 kB
Inactive:       78312192 kB
Active(anon):   24965120 kB
Inactive(anon):   212352 kB
Active(file):   11298752 kB
Inactive(file): 78099840 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       4194240 kB
SwapFree:        4194240 kB
Dirty:               512 kB
Writeback:             0 kB
AnonPages:      24776320 kB
Mapped:           157504 kB
Shmem:            406720 kB
Slab:           11619072 kB
SReclaimable:    3734336 kB
SUnreclaim:      7884736 kB
KernelStack:       34128 kB
PageTables:        69632 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    240269120 kB
Committed_AS:   59814336 kB
VmallocTotal:   549755813888 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
HardwareCorrupted:     0 kB
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
CmaTotal:       25165824 kB
CmaFree:        25165824 kB
HugePages_Total:   12800
HugePages_Free:    12800
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

其中 MemAvailable 可以用来评估系统当前应用程序可用的内存,定时监控该数据并显示出来便可得到系统内存使用曲线。

进程内存监控

系统可用内存一直减少时,说明系统中正在运行的程序有内存泄露,但还需要进一步确定是哪个程序在泄露。
/proc/{pid}/cmdline 里面记录了某一个进程(进程号为pid)的启动名称,这个名称就是进程的名字,因此遍历/proc文件夹下的所有进程文件夹就能找到与进程名字对应的进程PID。
/proc/{pid}/status 文件记录了进程的详细信息,其中 VmSize代表进程现在正在占用的内存,VmRSS是程序现在使用的物理内存,VmData:表示进程数据段的大小,Threads:表示当前进程中的线程个数,/proc/{pid}/smaps也存有进程更加详细的内存信息,Rss:是实际分配的内存,这部分物理内存已经分配,不需要缺页中断就可以使用的,Pss(proportional set size):是平摊计算后的实际物理使用内存(有些内存会和其他进程共享,例如mmap进来的)。以上数据对于进程内存监控和排查都具有重要意义

进程内存泄露点定位

已有的内存泄露检查工具

常用的内存泄露检查工具
Linux官方网站推荐的内存泄露检查工具

valgrind

valgrind 中的memcheck工具可以检查内存泄漏,如下所示的程序,总有两处泄漏

#include <vector>
#include <iostream>
#include <unistd.h>

class myClasses{
    public:
        void myFunc(){
            std::cout << "Hello World" << std::endl;
        }
        myClasses(){
            std::cout << "construct new" << std::endl;
            p = new int[100000];
        }
    private:
        int *p;
};

void leak_func(){
    auto p = new int;
    return;
}

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    for (int i = 0; i < v.size(); i++) {
        std::cout << v[i] << std::endl; 
    }
    for(int i = 0; i < 100; i++){
        leak_func();
    }
    {myClasses m;}
    pause();
    return 0;
}

采用 valgrind 检查泄露方法如下所示:

vito@vito-virtual-machine:~/valgrind_test$ valgrind --leak-check=full --show-leak-kinds=definite,indirect,possible,reachable  ./test
==30887== Memcheck, a memory error detector
==30887== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==30887== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==30887== Command: ./test
==30887== 
1
2
3
4
5
construct new
^C==30887== 
==30887== Process terminating with default action of signal 2 (SIGINT)
==30887==    at 0x4BA25F7: pause (pause.c:29)
==30887==    by 0x10943E: main (in /home/vito/valgrind_test/test)
==30887== 
==30887== HEAP SUMMARY:
==30887==     in use at exit: 474,148 bytes in 104 blocks
==30887==   total heap usage: 104 allocs, 0 frees, 474,148 bytes allocated
==30887== 
==30887== 20 bytes in 1 blocks are still reachable in loss record 1 of 5
==30887==    at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:487)
==30887==    by 0x109D77: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (in /home/vito/valgrind_test/test)
==30887==    by 0x109C1C: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (in /home/vito/valgrind_test/test)
==30887==    by 0x109A91: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (in /home/vito/valgrind_test/test)
==30887==    by 0x109892: void std::vector<int, std::allocator<int> >::_M_range_initialize<int const*>(int const*, int const*, std::forward_iterator_tag) (in /home/vito/valgrind_test/test)
==30887==    by 0x109627: std::vector<int, std::allocator<int> >::vector(std::initializer_list<int>, std::allocator<int> const&) (in /home/vito/valgrind_test/test)
==30887==    by 0x1093A6: main (in /home/vito/valgrind_test/test)
==30887== 
==30887== 400 bytes in 100 blocks are definitely lost in loss record 2 of 5
==30887==    at 0x4848F95: operator new(unsigned long) (vg_replace_malloc.c:487)
==30887==    by 0x10931E: leak_func() (in /home/vito/valgrind_test/test)
==30887==    by 0x109423: main (in /home/vito/valgrind_test/test)
==30887== 
==30887== 1,024 bytes in 1 blocks are still reachable in loss record 3 of 5
==30887==    at 0x484880F: malloc (vg_replace_malloc.c:446)
==30887==    by 0x4B36BA3: _IO_file_doallocate (filedoalloc.c:101)
==30887==    by 0x4B45CDF: _IO_doallocbuf (genops.c:347)
==30887==    by 0x4B44F5F: _IO_file_overflow@@GLIBC_2.2.5 (fileops.c:744)
==30887==    by 0x4B436D4: _IO_new_file_xsputn (fileops.c:1243)
==30887==    by 0x4B436D4: _IO_file_xsputn@@GLIBC_2.2.5 (fileops.c:1196)
==30887==    by 0x4B37FD6: fwrite (iofwrite.c:39)
==30887==    by 0x499AAF7: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30)
==30887==    by 0x49A9119: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30)
==30887==    by 0x1093E3: main (in /home/vito/valgrind_test/test)
==30887== 
==30887== 72,704 bytes in 1 blocks are still reachable in loss record 4 of 5
==30887==    at 0x484880F: malloc (vg_replace_malloc.c:446)
==30887==    by 0x4916939: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30)
==30887==    by 0x400647D: call_init.part.0 (dl-init.c:70)
==30887==    by 0x4006567: call_init (dl-init.c:33)
==30887==    by 0x4006567: _dl_init (dl-init.c:117)
==30887==    by 0x40202C9: ??? (in /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)
==30887== 
==30887== 400,000 bytes in 1 blocks are still reachable in loss record 5 of 5
==30887==    at 0x484A723: operator new[](unsigned long) (vg_replace_malloc.c:729)
==30887==    by 0x109562: myClasses::myClasses() (in /home/vito/valgrind_test/test)
==30887==    by 0x109439: main (in /home/vito/valgrind_test/test)
==30887== 
==30887== LEAK SUMMARY:
==30887==    definitely lost: 400 bytes in 100 blocks
==30887==    indirectly lost: 0 bytes in 0 blocks
==30887==      possibly lost: 0 bytes in 0 blocks
==30887==    still reachable: 473,748 bytes in 4 blocks
==30887==         suppressed: 0 bytes in 0 blocks
==30887== 
==30887== For lists of detected and suppressed errors, rerun with: -s
==30887== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

valgrind 工具在程序退出时会输出未释放内存信息,会有非常详细的信息输出,程序体量比较大的时候输出信息可能比较多。查找有用信息可能比较困难。

mtrace

mtrace 是GUN扩展函数。mtrace为内存分配函数(malloc, realloc, memalign, free)安装hook函数。这些hook函数记录内存的申请和释放的trace信息。在程序中,这些trace信息可以被用来发现内存泄漏和释放不是申请的内存。

//memLeakTest.cpp
#include <mcheck.h>
#include <stdlib.h>
#include <unistd.h>

void func_1(){
    for(int i = 0; i < 4; i++){
        void * p = malloc(128);
    }
    for(int i = 0; i < 2; i++){
        void * p = malloc(512);
    }
    return;
}
void func_2(){
    void * p = new int[128];
    return;
}

void func_3(){
    func_1();
    func_2();
    return;
}

int main()
{
    mtrace();
    func_3();
    // while(1){
    //     sleep(1);
    // }
    return 0;
}
sh-4.4$ export MALLOC_TRACE=mtrace.log
sh-4.4$ g++ -g -o memLeak memLeakTest.cpp 
sh-4.4$ ./memLeak 
sh-4.4$ mtrace ./memLeak mtrace.log 

Memory not freed:
-----------------
           Address     Size     Caller
0x000056448cee02b0     0x80  at 0x56448b868729
0x000056448cee0340     0x80  at 0x56448b868729
0x000056448cee03d0     0x80  at 0x56448b868729
0x000056448cee0460     0x80  at 0x56448b868729
0x000056448cee04f0    0x200  at 0x56448b86874a
0x000056448cee0700    0x200  at 0x56448b86874a
0x000056448cee0910    0x200  at 0x7fecd01ba298

生成的 mtrace.log如下所示:

= Start
@ ./memLeak:[0x55c8ff98a729] + 0x55c9001c62b0 0x80
@ ./memLeak:[0x55c8ff98a729] + 0x55c9001c6340 0x80
@ ./memLeak:[0x55c8ff98a729] + 0x55c9001c63d0 0x80
@ ./memLeak:[0x55c8ff98a729] + 0x55c9001c6460 0x80
@ ./memLeak:[0x55c8ff98a74a] + 0x55c9001c64f0 0x200
@ ./memLeak:[0x55c8ff98a74a] + 0x55c9001c6700 0x200
@ /usr/lib/x86_64-linux-gnu/libstdc++.so.6:(_Znwm+0x18)[0x7f9a4757e298] + 0x55c9001c6910 0x200

mtrace 只可以监控memalloc等函数的一级调用者,而且必须在程序正常退出之后才会记录l数据,当程序被kill杀掉时,不会记录数据。有的系统使用 mtrace 脚本无法生成调用地址的详细信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值