内存泄露与valgrind

    技术2025-01-28  8

    1 内存

    1.1 内存的相关概念介绍

    并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

    MMU 内存管理单元,完成虚拟地址与物理地址之间的映射。TLB CPU访问内存页表还是不够快,加了TLB,用来缓存页表,提高物理内存访问效率。页表 记录虚拟地址与物理地址的映射关系。页 内存映射的最小单位,也就是页,通常是 4 KB 大小。这样,每一次内存映射,都需要关联 4 KB 或者 4KB 整数倍的内存空间。多级页表 页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB/4KB),才可以实现整个地址空间的映射。 多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。缺页异常 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。大页 比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页通常用在使用大量内存的进程上,比如 DPDK 等。虚拟内存空间分布

    包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB,地址是从高地址开始增长的。  文件映射段: 包括动态库、共享内存等,从高地址开始向下增长。  堆: 包括动态分配的内存,从低地址开始向上增长。  数据段: 初始化的全局变量和static修饰的变量。  只读段: 包括代码和常量等。

    1.2 内存分配

     小块内存(小于 128K) C 标准库使用 brk() 来分配(malloc的底层是brk),也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。  大块内存(大于 128K) 内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。 mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc 只对大块内存使用 mmap 的原因。

    1.3 内存释放

    对应的API为free,unmap。  回收缓存 比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;  回收不常访问的内存 把不常用的内存通过交换分区直接写到磁盘中,换入与换出。 Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。 Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。  杀死进程 内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

    1.4 内存使用情况分析

    cat /proc/meminfo

    系统上内存使用情况的统计数据。其中常见项数据含义如下表:

    可用的物理内存=MemFree+Buffers+Cached echo 1 > /proc/sys/vm/drop_caches # free pagecache echo 2> /proc/sys/vm/drop_caches # free dentries and inodes echo 3> /proc/sys/vm/drop_caches #free pagecache, dentries and inodes cat /proc/sys/vm/min_free_kbytes #查看系统保留内存大小

    1.5 进程内存分析

    cat /proc/{pid}/maps,通过分析maps文件,分析进程当前运行时的stack,heap,mmap映射内存,是否持续增大,从而大概知道哪个进程发生了内存泄露。

    1.6 内存泄露类型

    常发性内存泄漏,泄露部分代码每执行一次,就泄露一块内存。偶发性内存泄露,偶然发生内存泄露。一次性内存泄露,内存泄露的代码只会被执行一次,发生一次泄露。隐式内存泄露,分配了大量的内存,由于程序一直在运行,内存就一直占用,得不到释放。

    1.7 常见的内存错误

    使用未初始化的内存,局部变量和动态分配的内存,没有初始化就拿来使用。内存读写越界,数组越界操作。内存覆盖,strcpy拷贝字符串。动态内存管理错误,c++ malloc的内存,使用delete释放,释放了两次,如果一块内存释放了,操作系统会回收,被其它地方申请使用,如果这边再次释放,会导致其它地方产生不可预知的错误。

    1.8 常见的内存工具

    常用的工具有free/top/htop/sar/vmstat等 free #参数来源于/proc/meminfo,以下各参数解析参考1.4节内存使用情况分析

    top 各参数含义:

    htop

    vmstat 1 1000 #每1s运行一次,总共运行1000次

    2 valgrind

    2.1 valgrind概述

    valgrind是一个运行在linux下的仿真工具集,其中包括以下常用工具。  memcheck 内存检查器,能发生大部分的内存错误,比如未初始以化的内存,使用释放的内存,内存越界等。  massif 堆分析器,提供了非常详细的堆内存分配信息。  helgrind 检测多线程的错误问题,包括,线程API的使用错误,数据同步(访问共享资源没有加锁保护)等。  cachegrind 模拟程序如何与机器的缓存层次结构和(可选的)分支交互预测。

    2.2 memcheck 内存错误分析

    memcheck可以检测出,使用未初始化的内存,double free(双重释放),内存越界,内存泄露等,下面结合这些内存错误来分析memcheck的用法。

    1. 使用未初始化的内存 代码:01memcheck.c 编程程序并运行 gcc 01memcheck.c -g -O0

    valgrind --tool=memcheck --track-origins=yes ./a.out 参数: –track-origins # show origins of undefined values

    查看valgrind memcheck检测报告,可以看出,程序运行到每13行时,使用到了第10行代码的未初始化栈变量。

    2. double free(双重释放堆内存) 代码:02memcheck.c 编程程序并运行 gcc 02memcheck.c -g -O0

    valgrind --tool=memcheck ./a.out

    从报告可以看出,代码23行分析的堆内存,被释放了两次

    3. 内存越界 代码:03memcheck.c 编程程序并运行 gcc 03memcheck.c -g -O0

    valgrind --tool=memcheck --track-origins=yes ./a.out

    从程序运行报告得知,实际上代码11行只分配了5字节,但是在代码14行,有一字节的内存是无效访问。

    4. 内存泄露 上面的例子有些简单,而且问题很容易看出来,下面分析一下复杂的例子,检测内存泄露问题。

    如图所示,client向server发送请求,server回复json数据,client解析json数据。

    案例代码: https://github.com/jorinzou/linux_command/tree/master/valgrind/memcheck/mem_leak

    为了避免先入为主,先不要看代码,分别编译client和server并运行起来,其中client在运行之前,先在config中配置server的ip

    可配置成本地网卡任意一个ip

    分别在xshell的两个窗口开两个窗口,分别启动server和client

    执行free -m -c 10000 1 执行free命令,查看内存,总共运行10000次,每1s查看一次内存使用情况。 从图中可以看出,系统的使用内存正有规律地减少。

    这个时候,valgrind memcheck用起来。 使用valgrind启动client。 valgrind --tool=memcheck --trace-children=yes --show-leak-kinds=all --log-file=/tmp/client.md –trace-children=yes #跟踪所有的父进程及其相关子进程 –show-leak-kinds=all --leak-check=full #检测所有泄露类型。 –log-file=/tmp/client.md #把检测报告记录到文件/tmp/client.md

    等待几min后,停止client,查看/tmp/client.md文件。

    先来分析一下,这个名词是什么意思。 definitely lost (17M左右) 确定的内存泄露 indirectly lost (47M左右) 间接的,没有任何指针指向该内存 possibly lost (5KB左右) 可能的内存泄露,指某个指针访问某块内存,但这块内存已经不是这块首地址了。

    查看内存分配调用栈,可以大概知道分析堆内存的函数调用栈。

    这两行代码负责json数据的解析与打印。

    占进去查看,发现cJSON_Parse和 cJSON_Print都内部都调用了malloc分配了堆内存,所以也要相应的释放。

    处理方法,释放内存:

    执行free -m查看,内存基本没有再减少了。

    再次查看修改后的valgrind memleak分析报告:

    修复之后,几处内存泄露的点,值都为0。

    2.3 massif 堆内存分析

    可以分析堆内存的分配详细信息。 编译运行以下程序,其中client按照下面的方式运行

    案例代码: https://github.com/jorinzou/linux_command/tree/master/valgrind/massif

    valgrind --tool=massif --trace-children=yes --heap=yes --massif-out-file=massif.out.%p ./client

    各参数含义: –trace-children=yes #同时跟踪子线程 –heap=yes #分析堆信息使能 –massif-out-file=massif.out.%p #分析文件按照这种格式输出

    运行一段时间后,得到以下文件

    打开查看 可以得出堆内存分配情况,及函数调用堆栈。

    2.4 helgrind 线程分析

    检测多线程的错误问题,包括,线程API的使用错误,数据同步(访问共享资源没有加锁保护)等。 下面以访问共享资源没有加锁保护为案例,分析一下helgrind的使用方法。 案例代码: https://github.com/jorinzou/linux_command/tree/master/valgrind/helgrind/pthread_lock 编译: gcc mutex.c -g -O0 -lpthread 运行: valgrind --tool=helgrind --track-lockorders=yes --trace-children=yes ./a.out 参数: –track-lockorders=yes #表示进行锁检测

    查看检测报告: 从报告可以看出,线程Thread2Task,Thread1Task,Thread3Task有4字节数据存在竞争,没有加锁保护。

    2.5 cachegrind cache分析

    模拟程序如何与机器的缓存层次结构和(可选的)分支交互预测。 编译运行以下代码实例: https://github.com/jorinzou/linux_command/tree/master/valgrind/cachegrind

    其中客户端以以下方式运行: valgrind --tool=cachegrind --cache-sim=yes --branch-sim=yes --cachegrind-out-file=cache.%p ./client

    运行一段时间后,停止,查看停止后的报告分析:

    从表中,可以得到指令cache和数据cache的miss情况。

    打开文件: cache.33977

    从报告中可以得出,I1,D1 cache的使用情况。

    2.6 总结

    valgrind只能分析随着进程一起启动的相关(内存,线程,cache)的信息,但是对于正在运行的程序就无能为力了,这时就要结合其它工具综合分析,如下:  系统已用,可用,剩余内存:free,vmstat,sar,proc/meminfo。  进程虚拟内存,常驻内存,共享内存:ps,top。  进程内存分布:/proc/[pid]/maps。  缓存/缓冲区用量:free,vmstat,sar,bcc工具集合中的(cachestat)。  进程换页情况:sar。  进程缺页情况:ps ,top。

    参考:

    http://linuxperf.com/?p=142 http://www.valgrind.org/ https://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/ 《极客时间-倪朋飞-linux性能优化-内存优化》

    Processed: 0.009, SQL: 9