【Linux】使用Valgrind定位内存增长问题

16 篇文章 0 订阅

1 内存问题

内存问题是一类比较难以定位的问题,通常有两类场景:

  • 程序在低负载情况下的内存使用量是否正常,低负载情况下不应该太高
  • 程序在高负载情况下的内存使用量是否正常,高负载情况下应该为内存使用量设置阈值,防止触发OOM

因此,为了检查内存使用量是否正常,需要知道当前的内存使用量以及哪里分配的内存造成内存增长

使用topps命令可以查看某个进程的内存使用量,然后结合代码中初始分配的内存大小评估当前的内存使用量是否正常。

2 Valgrind

2.1 Valgrind介绍

Valgrind是Linux上的一套开源的动态分析工具集,通常用来检测和分析程序中的错误,提高程序的稳定性和性能。

Valgrind整体架构上包含内核和周边工具集,将程序放到内核模拟的仿真环境中运行,并提供一些能力接口,然后基于这些接口实现周边工具。

2.2 Valgrind中的Memcheck

Memcheck是最常用且是默认的工具,通常用于检查内存泄漏等问题。

示例1:

#include <iostream>

int main() {
	int *ptr = nullptr;
	*ptr = 0;

	return 0;
}

编译并执行:

g++ -o mem3 -g mem3.cpp
valgrind --leak-check=full ./mem3

输出:

==43290== Memcheck, a memory error detector
==43290== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==43290== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==43290== Command: ./mem3
==43290== 
==43290== Invalid write of size 4
==43290==    at 0x10917D: main (mem3.cpp:5)
==43290==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==43290== 
==43290== 
==43290== Process terminating with default action of signal 11 (SIGSEGV)
==43290==  Access not within mapped region at address 0x0
==43290==    at 0x10917D: main (mem3.cpp:5)
==43290==  If you believe this happened as a result of a stack
==43290==  overflow in your program's main thread (unlikely but
==43290==  possible), you can try to increase the size of the
==43290==  main thread stack using the --main-stacksize= flag.
==43290==  The main thread stack size used in this run was 8388608.
==43290== 
==43290== HEAP SUMMARY:
==43290==     in use at exit: 0 bytes in 0 blocks
==43290==   total heap usage: 1 allocs, 1 frees, 72,704 bytes allocated
==43290== 
==43290== All heap blocks were freed -- no leaks are possible
==43290== 
==43290== For lists of detected and suppressed errors, rerun with: -s
==43290== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Segmentation fault

从上面的输出可以看出:

  • mem3.cpp的第5行进行了地址的非法写操作
  • 程序收到SIGSEGV信号而终止,是由于访问了0x0地址
  • 所有的堆内存都被释放,没有内存泄漏

示例2:

#include <iostream>

int main() {
	int *ptr = (int*)malloc(sizeof(int));
	*ptr = 0;

	return 0;
}

输出:

==45775== Memcheck, a memory error detector
==45775== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==45775== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==45775== Command: ./mem4
==45775== 
==45775== 
==45775== HEAP SUMMARY:
==45775==     in use at exit: 4 bytes in 1 blocks
==45775==   total heap usage: 2 allocs, 1 frees, 72,708 bytes allocated
==45775== 
==45775== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==45775==    at 0x4843839: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==45775==    by 0x10919E: main (mem4.cpp:4)
==45775== 
==45775== LEAK SUMMARY:
==45775==    definitely lost: 4 bytes in 1 blocks
==45775==    indirectly lost: 0 bytes in 0 blocks
==45775==      possibly lost: 0 bytes in 0 blocks
==45775==    still reachable: 0 bytes in 0 blocks
==45775==         suppressed: 0 bytes in 0 blocks
==45775== 
==45775== For lists of detected and suppressed errors, rerun with: -s
==45775== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

从上面的输出可以看出:

  • in use at exit表明有4个字节在程序退出时还在使用,下面的4 bytes in 1 blocks are definitely lost也表明有内存没有释放
  • LEAK SUMMARY是对内存泄漏的汇总信息

Memcheck可以检测程序中的常见的内存分配但不释放、访问越界等问题,但是,有时候有指针指向内存块,只是程序还没有释放且继续申请内存,运行一段时间后由于OOM退出,这时候Memcheck就无能为力了,而且现代的程序比较复杂,Memcheck会有很多误告。

2.3 Valgrind中的Massif

内存持续增长一般有两种场景:

  • 出现死循环或者递归不终止,造成栈溢出
  • 持续分配堆内存,而不释放,此时可能存在内存泄漏或者是程序逻辑处理异常而没有正确释放内存(当然,也有可能是死循环引起的)

对于第一个问题,可以通过日志和进程当前的快照确认,而且,出现这种情况时通常会造成线程的CPU高。

对于第二个问题,就是要知道程序在哪里出现了持续分配堆内存,然后可以审查这部分代码,看是否存在没有释放的场景。那怎么知道程序在哪里分配堆内存呢?就要用到Valgrind中的Massif工具。

Massif工具通过采样的方式获取当前内存分配的情况,并且可以输出分配内存的调用栈。

示例代码:

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

void func() {
	while(true) {
		char *ptr = (char *)malloc(1024*sizeof(char));
		sleep(1);
	}
}

int main() {
	func();

	return 0;
}

使用Valgrind执行该程序:valgrind --tool=massif --time-unit=B ./mem5,然后将输出文件转换成文本:ms_print massif.out.47145 > memory.log

结果文件分为三个部分。

第一部分是一个简单的说明,表明生成该采样文件执行的命令、Massif和ms_print的参数。

由于Massif工具的工作方式是对堆内存的使用情况进行快照,因此,第二部分的坐标图和第三部分的表格记录的都是快照的情况,有的快照包含内存的使用量和分配堆内存的调用栈,有的快照只包含内存的使用量,因此将快照分为详细快照普通快照

第二个部分是一个用符号生成的坐标图:

    KB
122.4^                                             ##########################
     |                                           :@#
     |                                         :::@#
     |                                      ::@:::@#
     |                                    @:::@:::@#
     |                                  ::@:::@:::@#
     |                                :@::@:::@:::@#
     |                             ::::@::@:::@:::@#
     |                           ::@:::@::@:::@:::@#
     |                          :::@:::@::@:::@:::@#
     |                          :::@:::@::@:::@:::@#
     |                          :::@:::@::@:::@:::@#
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
     |                          :::@:::@::@:::@:::@#                         :
   0 +----------------------------------------------------------------------->KB
     0                                                                   193.4

Number of snapshots: 55
 Detailed snapshots: [9, 19, 29, 39, 49, 53 (peak)]

坐标图的横轴表示采样的间隔,不同的单位有不同的含义:

  • i:默认的单位,表示每次快照之间的指令执行数量
  • ms:毫秒,表示从程序启动之后的时间
  • B:字节,表示分配的内存量,单位是B

坐标图的纵坐标表示分配的堆内存大小,所以从图里面可以直观的看出堆内存分配的趋势。

坐标图中的符号的含义:

  • 冒号:该字符组成的柱状图表示普通快照,只记录内存的使用量
  • @:该字符组成的柱状图表示详细快照,记录内存的使用量和分配内存的调用栈
  • 井号:该字符组成的柱状图表示峰值快照,记录内存的使用量和分配内存的调用栈

在坐标图的下面还列出了记录快照的数量:

  • 总共记录了55个快照
  • 详细快照有6个,里面也包含峰值快照

因此,看第二个部分就可以看到大概的内存增长趋势:堆内存是一直增长的。

第三部分是记录的所有快照的数据,以前10个快照为例:

--------------------------------------------------------------------------------
  n        time(B)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
  0              0                0                0             0            0
  1         72,712           72,712           72,704             8            0
  2         73,744           73,744           73,728            16            0
  3         74,776           74,776           74,752            24            0
  4         75,808           75,808           75,776            32            0
  5         76,840           76,840           76,800            40            0
  6         77,872           77,872           77,824            48            0
  7         78,904           78,904           78,848            56            0
  8         79,936           79,936           79,872            64            0
  9         80,968           80,968           80,896            72            0
99.91% (80,896B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.
->89.79% (72,704B) 0x4903B79: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.29)
| ->89.79% (72,704B) 0x4011B1D: call_init.part.0 (dl-init.c:70)
|   ->89.79% (72,704B) 0x4011C07: call_init (dl-init.c:33)
|     ->89.79% (72,704B) 0x4011C07: _dl_init (dl-init.c:117)
|       ->89.79% (72,704B) 0x4001109: ??? (in /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)
|
->10.12% (8,192B) 0x1091BE: func() (mem5.cpp:6)
  ->10.12% (8,192B) 0x1091DB: main (mem5.cpp:12)

快照的信息组成一个表格:

  • n:快照的序号,从0开始,对应的就是第二部分中详细快照中的序号
  • time:时间,对应的是坐标图中的横轴,这里的time_unit设置的是B,因此,横轴的单位是B,而且由于横坐标是B,横纵坐标值是一样的
  • total:分配的内存总量,对应的是坐标图中的纵轴,单位是B
  • useful-heap:程序申请的内存量
  • extra-heap:超过程序申请的内存量,例如管理内存需要的元数据,一般来说,这部分在整体分配的内存中的占比可以忽略
  • stacks:栈的大小,默认情况下,栈的分析是关闭的,会降低Massif的速度,可以通过--stacks=yes开启

普通快照详细快照都会包含上述信息,而详细快照还包含分配这些内存的调用栈。

例如,序号为9的快照就是详细快照,首先给出了程序申请的内存(useful-heap)占内存总量(total)的占比以及堆分配函数(malloc/new/new[]):

99.91% (80,896B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.

然后就是分配内存的堆栈,这里包含两个栈,第一个栈是:

->89.79% (72,704B) 0x4903B79: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.29)
| ->89.79% (72,704B) 0x4011B1D: call_init.part.0 (dl-init.c:70)
|   ->89.79% (72,704B) 0x4011C07: call_init (dl-init.c:33)
|     ->89.79% (72,704B) 0x4011C07: _dl_init (dl-init.c:117)
|       ->89.79% (72,704B) 0x4001109: ??? (in /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)

该堆栈是Valgrind自身分配的内存,一般可以忽略。

第二个栈是:

->10.12% (8,192B) 0x1091BE: func() (mem5.cpp:6)
  ->10.12% (8,192B) 0x1091DB: main (mem5.cpp:12)

从该调用栈就可以知道8192B是从哪里分配的,注意调用栈是下面调用上面,也就是说,mem5.cpp的第6行的func()函数中的代码分配的。

有时候在调用栈中还会看到02.18% (1,891,902B) in 546 places, all below massif's threshold (1.00%)这样的描述,是因为有些小的内存分配对整体影响不大就没有记录调用栈,一般都可以直接忽略,如果想看到所有的调用栈,可以设置--threshold参数。

3 总结

当程序运行过程中,内存持续增长,一段时间后就可能造成OOM,此时可以使用Valgrind的Massif工具分析程序执行过程中的堆分配内存量。

在使用Valgrind的Massif工具运行程序后就可以得到进程在运行过程中的堆内存分配情况以及对应的栈,对分配内存较多的代码进行审查并优化,或者添加一些限制,减少分配的内存。

使用过程中需要注意两个地方:

  • Massif工具只能用来分析堆内存的使用,也就是通过malloc/new/new[]分配的内存,有些可能是通过mmap分配的内存就没办法分析了
  • 对于运行时间较短的程序,需要加上--time-unit=B参数
  • 14
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值