Valgrind内存检查工具

引言

         C/C++语言中,内存的分配与回收都是由开发人员在编写代码时主动完成的,好处是内存管理的开销较小,程序拥有更高的执行效率;弊端是依赖于开发者的水平,随着代码规模的扩大,极容易遗漏释放内存的步骤,或者一些不规范的编程可能会使程序具有安全隐患。如果对内存管理不当,可能导致程序中存在内存缺陷,甚至会在运行时产生内存故障错误。

        内存泄漏是各类缺陷中十分棘手的一种,对系统的稳定运行威胁较大。当动态分配的内存在程序结束之前没有被回收时,则发生了内存泄漏。由于系统软件,如操作系统、编译器、开发环境等都是由C/C++语言实现的,不可避免地存在内存泄漏缺陷,特别是一些在服务器上长期运行的软件,若存在内存泄漏则会造成严重后果,例如性能下降、程序终止、系统崩溃、无法提供服务等。

概念

        内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。当我们在程序中对原始指针(raw pointer)使用new操作符或者free函数的时候,实际上是在堆上为其分配内存,这个内存指的是RAM,而不是硬盘等永久存储。持续申请而不释放(或者少量释放)内存的应用程序,最终因内存耗尽导致OOM(out of memory)

        方便大家理解内存泄漏的危害,我们举个简单的例子。有一个宾馆,有100间房间,顾客每次都是在前台进行登记,然后拿到房间钥匙,如果有些顾客不需要该房间了,也不归还钥匙,久而久之,前台处可用的房间越来越少,收入也越来越少,濒临倒闭。同理,程序申请了内存,而不进行归还,久而久之,可用的内存越来越少,操作系统就会进行自我保护,杀掉该进程,这就是我们常说的OOM(out of memory)。

内存泄漏分为以下两类:

  • 堆内存泄漏:我们经常说的内存泄漏就是堆内存泄漏,在堆上申请了资源,在结束使用的时候,没有释放归还给OS,从而导致该块内存永远不会被再次使用
  • 资源泄漏:通常指的是系统资源,比如socket,文件描述符等,因为这些在系统中都是有限制的,如果创建了而不归还,久而久之,就会耗尽资源,导致其他程序不可用

本文主要分析堆内存泄漏,所以后面的内存泄漏均指的是堆内存泄漏

根源

        内存泄漏,主要指的是在堆(heap)上申请的动态内存泄漏,或者说是指针指向的内存块忘了被释放,导致该块内存不能再被申请重新使用.

堆与栈区别:理解堆和栈的区别,对我们开发过程中会非常有用,二者的区别如下。

对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak

  • 空间大小不同
    • 一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。
    • 对于栈来讲,一般都是有一定的空间大小的,一般依赖于操作系统(也可以人工设置  ulimit -s 102400 /)
  • 能否产生碎片不同
    • 对于堆来讲,频繁的内存分配和释放势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
    • 对于栈来讲,内存都是连续的,申请和释放都是指令移动,类似于数据结构中的进栈和出栈
  • 增长方向不同
    • 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向
    • 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长
  • 分配方式不同
    • 堆都是动态分配的,比如我们常见的malloc/new;而栈则有静态分配和动态分配两种。
    • 静态分配是编译器完成的,比如局部变量的分配,而栈的动态分配则通过alloca()函数完成
    • 二者动态分配是不同的,栈的动态分配的内存由编译器进行释放,而堆上的动态分配的内存则必须由开发人自行释放
  • 分配效率不同
    • 栈有操作系统分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高
    • 堆内存的申请和释放专门有运行时库提供的函数,里面涉及复杂的逻辑,申请和释放效率低于栈

        截止到这里,栈和堆各自的优缺点、使用场景已经分析完成,建议能使用栈的时候,就尽量使用栈,一方面是因为效率高于堆,另一方面内存的申请和释放由编译器完成,这样就避免了很多问题。

产生方式

以产生的方式来分类,内存泄漏分为以下四类。

  • 常发性内存泄漏
  • 偶发性内存泄漏
  • 一次性内存泄漏
  • 隐式内存泄漏
常发性内存泄漏
    产生内存泄漏的代码或者函数会被多次执行到,在每次执行的时候,都会产生内存泄漏。
偶发性内存泄漏
    与常发性内存泄漏不同的是,偶发性内存泄漏函数只在特定的场景下才会被执行。
一次性内存泄漏
    这种内存泄漏在程序的生命周期内只会泄漏一次,或者说造成泄漏的代码只会被执行一次。这种严格意义上,并不算内存泄漏,因为程序是这么设计的,即使程序异常退出,那么整个服务进程也就退出了。
隐式内存泄漏
    程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放

问题定位

1)普通方式——日志

        其主要原理是在分配的时候,统计分配次数,在释放的时候,则是统计释放的次数,这样在程序结束前判断这俩值是否一致,就能判断出是否存在内存泄漏。(暂无考虑线程安全)

        此方法可帮助跟踪已分配内存的状态。为了实现这个方案,需要创建三个自定义函数,一个用于内存分配,第二个用于内存释放,最后一个用于检查内存泄漏。

示例代码:

static unsigned int allocated  = 0;
static unsigned int deallocated  = 0;
void *Memory_Allocate (size_t size)
{
    void *ptr = NULL;
    ptr = malloc(size);
    if (NULL != ptr) {
        ++allocated;
    } else {
        //Log error
    }
    return ptr;
}
void Memory_Deallocate (void *ptr) {
    if(pvHandle != NULL) {
        free(ptr);
        ++deallocated;
    }
}
int Check_Memory_Leak(void) {
    int ret = 0;
    if (allocated != deallocated) {
        //Log error
        ret = MEMORY_LEAK;
    } else {
        ret = OK;
    }
    return ret;
}

2)GDB调试

gdb内部调试检测是否存在内存泄漏,只能检测一些比较简单的内存泄漏,在复杂的环境里,难以使用,效率不高。

call malloc_stats()        系统函数在malloc.h里

call malloc_info(0, stdout) 

示例代码:

#include <stdlib.h>
#include <unistd.h>
 
void test_malloc_leak(int size)
{
    malloc(1024);
}
 
 
void no_leak()
{
    void *p = malloc(1024*1024*10);
    free(p);
}
 
 
int main(int argc, char *argv[])
{
    no_leak();
    test_malloc_leak(1024);
    return 0;
}

gdb调试过程:

1.malloc_stats()系统函数。

(gdb) b 20
Breakpoint 1 at 0x1191: file leak.c, line 20.
(gdb) r
Starting program: /home/wang/a.out

Breakpoint 1, no_leak () at leak.c:20
20 void *p = malloc(1024*1024*10);
(gdb) n
21 free(p);
(gdb) call malloc_stats()
Arena 0:
system bytes = 135168
in use bytes = 656
Total (incl. mmap):
system bytes = 10625024
in use bytes = 10490512
max mmap regions = 1
max mmap bytes = 10489856
(gdb) n
22 }
(gdb) call malloc_stats()
Arena 0:
system bytes = 135168
in use bytes = 656
Total (incl. mmap):
system bytes = 135168
in use bytes = 656
max mmap regions = 1
max mmap bytes = 10489856
(gdb) n
main (argc=1, argv=0x7fffffffdeb8) at leak.c:27
27 test_malloc_leak(1024);
(gdb) n
28 return 0;
(gdb) call malloc_stats()
Arena 0:
system bytes = 135168
in use bytes = 1696
Total (incl. mmap):
system bytes = 135168
in use bytes = 1696
max mmap regions = 1
max mmap bytes = 10489856
(gdb) p 1696 - 656
$1 = 1040
(gdb)

2.malloc_infp(0,stdout) 第一个参数始终为0,第二个参数是一个文件,可以通过标准输出显示结果。malloc_info()输出的是xml格式,通过比较其中rest字段来判断是否有内存泄漏。

main (argc=1, argv=0x7fffffffdeb8) at leak.c:27
27 test_malloc_leak(1024);
(gdb) call malloc_info(0,stdout)
<malloc version="1">
<heap nr="0">
<sizes>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="1" size="133472"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</heap>
<total type="fast" count="0" size="0"/>
<total type="rest" count="1" size="133472"/>
<total type="mmap" count="0" size="0"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</malloc>
$7 = 0
(gdb) n
28 return 0;
(gdb) call malloc_info(0,stdout)
<malloc version="1">
<heap nr="0">
<sizes>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="1" size="132432"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</heap>
<total type="fast" count="0" size="0"/>
<total type="rest" count="1" size="132432"/>
<total type="mmap" count="0" size="0"/>
<system type="current" size="135168"/>
<system type="max" size="135168"/>
<aspace type="total" size="135168"/>
<aspace type="mprotect" size="135168"/>
</malloc>
$8 = 0
(gdb) p 133472-132432
$9 = 1040
(gdb)

系统申请了1024字节没有释放。

3)第三方工具——Valgrind

        在Linux上比较常用的内存泄漏检测工具是valgrind,所以咱们就以valgrind为工具,进行检测。下载方式(Valgrind: Current Releases)也可在yocto中将valgrind工具添加。

使用说明:

  • 待检测的程序编译时需加入"-g",保留调试信息
  • 检测命令格式:valgrind --tool=memcheck --leak-check=full ./file,–leak-check=full表示检测所有内存泄露 ,--tool=memcheck表示指定tools为memcheck,默认是memcheck。
  • 检测记录除了输出到终端,还可以输出到文件,这样可以监测程序长时间运行后出现异常,通过查阅文件记录分析问题。--log-file-exactly=<file>   输出log信息到file
  • 更多的使用说明或者忘记命令时,可以执行valgrind -h查看帮助信息

示例代码:

#include <stdio.h>
#include <unistd.h>
 
void func_c(void)
{
    char *buff = (char *)malloc(1024);
}
 
int main(int argc,char **argv)
{
    while(1)
    {
        func_c();
        sleep (1);
    }
    return 0;
}
  • 通过gcc -g leak.c -o leak命令进行编译 

  • 执行命令: valgrind --leak-check=full   ./leak

==35940== Memcheck, a memory error detector
==35940== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==35940== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==35940== Command: ./leak
==35940==
^C==35940==
==35940== Process terminating with default action of signal 2 (SIGINT)
==35940== at 0x493D1B4: clock_nanosleep@@GLIBC_2.17 (clock_nanosleep.c:78)
==35940== by 0x4942EC6: nanosleep (nanosleep.c:27)
==35940== by 0x4942DFD: sleep (sleep.c:55)
==35940== by 0x1091A7: main (wt.c:14)
==35940==
==35940== HEAP SUMMARY:
==35940== in use at exit: 60 bytes in 6 blocks
==35940== total heap usage: 6 allocs, 0 frees, 60 bytes allocated
==35940==
==35940== 60 bytes in 6 blocks are definitely lost in loss record 1 of 1
==35940== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==35940== by 0x10917E: func_c (wt.c:6)
==35940== by 0x10919D: main (wt.c:13)
==35940==
==35940== LEAK SUMMARY:
==35940== definitely lost: 60 bytes in 6 blocks
==35940== indirectly lost: 0 bytes in 0 blocks
==35940== possibly lost: 0 bytes in 0 blocks
==35940== still reachable: 0 bytes in 0 blocks
==35940== suppressed: 0 bytes in 0 blocks
==35940==
==35940== For lists of detected and suppressed errors, rerun with: -s
==35940== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

valgrind的检测信息将内存泄漏分为如下几类:

  • definitely lost:确定产生内存泄漏
  • indirectly lost:间接产生内存泄漏
  • possibly lost:可能存在内存泄漏
  • still reachable:即使在程序结束时候,仍然有指针在指向该块内存,常见于全局变量

主要关注上面输出的下面几句:

==35940== by 0x10917E: func_c (wt.c:6)
==35940== by 0x10919D: main (wt.c:13)

提示在main函数(leak.c的第6行)fun函数(leak.c的第13行)产生了内存泄漏,通过分析代码,原因定位,问题解决。

valgrind不仅可以检测内存泄漏,还有其他很强大的功能,由于本文以内存泄漏为主,有兴趣的可以通过valgrind --help来进行查看。

总结

        在C/C++开发过程中,内存泄漏是一个非常常见的问题,其影响相对来说远低于coredump等,所以遇到内存泄漏的时候,不用过于着急,大不了重启嘛。

在开发过程中遵守下面的规则,基本能90+%避免内存泄漏:

  • 良好的编程习惯,只有有malloc/new,就得有free/delete
  • 也是最重要的一点,谁申请,谁释放

        对于malloc分配内存,分配失败的时候返回值为NULL,此时程序可以直接退出了。如果我们上线后,发现程序存在内存泄漏,如果不严重的话,可以先暂时不管线上,同时进行排查定位。在定位问题点的时候,可以采用缩小范围法,着重分析这次新增的代码,这样能够有效缩短问题解决的时间。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值