linux性能优化内存泄漏的定位和处理

Posted sysu_lluozh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux性能优化内存泄漏的定位和处理相关的知识,希望对你有一定的参考价值。

对普通进程来说,能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由
系统映射为物理内存

当进程通过malloc()申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访问时,才通过缺页异常陷入内核中分配内存

为了协调CPU与磁盘间的性能差异,Linux还会使用Cache和Buffer,分别把文件和磁盘读写的数据缓存到内存中

对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。管理内存的过程中,也很容易发生各种各样的"事故",比如:

  1. 没正确回收分配后的内存,导致了泄漏
  2. 访问的是已分配内存边界外的地址,导致程序异常退出

那内存泄漏到底是怎么发生的,以及发生内存泄漏之后该如何排查和定位呢?

说起内存泄漏,这就要先从内存的分配和回收说起

一、内存的分配和回收

回顾一下,有哪些方法来分配内存?用完后又该怎么释放还给系统呢?

1.1 内存段

用户空间内存包括多个不同的内存段,比如:

  • 只读段
  • 数据段
  • 文件映射段

这些内存段正是应用程序使用内存的基本方式

1.2 栈内存

举个例子,在程序中定义了一个局部变量,比如一个整数数组int data[64],就定义了一个可以存储64个整数的内存段。由于这是一个局部变量,它会从内存空间的栈中分配内存

栈内存由系统自动分配和管理

一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题

1.3 堆内存

很多时候,事先并不知道数据大小,所以要用到标准库函数malloc() __在程序中动态分配内存,这时候系统就会从内存空间的堆中分配内存

堆内存由应用程序自己来分配和管理

除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数free()来释放它们
如果应用程序没有正确释放堆内存,就会造成内存泄漏

1.4 其他内存泄漏的内存块

上面是两个栈和堆的例子,那么其他内存段是否也会导致内存泄漏呢?

  • 只读段

包括程序的代码和常量
由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏

  • 数据段

包括全局变量和静态变量
这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏

  • 内存映射段

包括动态链接库和共享内存
其中共享内存由程序动态分配和管理,所以,如果程序在分配后忘了回收就会导致跟堆内存类似的泄漏问题

1.5 内存泄漏的危害

内存泄漏的危害非常大,这些忘记释放的内存不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用
内存泄漏不断累积,甚至会耗尽系统内存

虽然,系统最终可以通过OOM(Out of Memory)机制杀死进程,但进程OOM前可能已经引发了一连串的反应,导致严重的性能问题

比如,其他需要内存的进程可能无法分配新的内存;内存不足又会触发系统的缓存回收以及 SWAP机制,从而进一步导致I/O的性能问题等等

内存泄漏的危害这么大,那应该怎么检测这种问题呢?特别是如果已经发现了内存泄漏,该如何定位和处理呢

二、案例

用一个计算斐波那契数列的案例,来看看内存泄漏问题的定位和处理方法

2.1 功能背景

斐波那契数列是一个这样的数列:

0、1、1、2、3、5、8…

也就是除了前两个数是0和1,其他数都由前面两数相加得到,用数学公式来表示就是

F(n)=F(n-1)+F(n-2)(n>=2),F(0)=0, F(1)=1

2.2 环境准备

预先安装sysstatDocker以及bcc软件包,比如:

# install sysstat docker
sudo apt‑get install ‑y sysstat docker.io
# Install bcc
sudo apt‑key adv ‑‑keyserver keyserver.ubuntu.com ‑‑recv‑keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/bionic bionic main" | sudo tee /etc/apt/sources.list.d/iovisor
sudo apt‑get update
sudo apt‑get install ‑y bcc‑tools libbcc‑examples linux‑headers‑$(uname ‑r)

其中,sysstat软件包中的vmstat,可以观察内存的变化情况

bcc软件包提供了一系列的Linux性能分析工具,常用来动态追踪进程和内核的行为,提供的所有工具都位于/usr/share/bcc/tools这个目录中

安装完成后,再执行下面的命令来运行案例:

$ docker run ‑‑name=app ‑itd feisky/app:mem‑leak

案例成功运行后,输入下面的命令确认案例应用已经正常启动:

$ docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13

从输出中,可以发现这个案例会输出斐波那契数列的一系列数值。实际上,这些数值每隔1秒输出一次

2.3 检查内存情况

那怎么检查内存情况,判断有没有泄漏发生呢?
首先想到的可能是top工具,不过top虽然能观察系统和进程的内存占用情况,但这个的案例并不适合。内存泄漏问题更应该关注内存使用的变化趋势,所以接下来使用vmstat工具

运行下面的vmstat,等待一段时间,观察内存的变化情况

# 每隔3秒输出一组数据
$ vmstat 3
procs ‑‑‑‑‑‑‑‑‑‑‑memory‑‑‑‑‑‑‑‑‑‑ ‑‑‑swap‑‑ ‑‑‑‑‑io‑‑‑‑ ‑system‑‑ ‑‑‑‑‑‑cpu‑‑‑‑‑
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
procs ‑‑‑‑‑‑‑‑‑‑‑memory‑‑‑‑‑‑‑‑‑‑ ‑‑‑swap‑‑ ‑‑‑‑‑io‑‑‑‑ ‑system‑‑ ‑‑‑‑‑‑cpu‑‑‑‑‑
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
0  0      0 6601824  97620 1098784    0    0     0     0   62  322  0  0 100  0  0
0  0      0 6601700  97620 1098788    0    0     0     0   57  251  0  0 100  0  0
0  0      0 6601320  97620 1098788    0    0     0     3   52  306  0  0 100  0  0
0  0      0 6601452  97628 1098788    0    0     0    27   63  326  0  0 100  0  0
2  0      0 6601328  97628 1098788    0    0     0    44   52  299  0  0 100  0  0
0  0      0 6601080  97628 1098792    0    0     0     0   56  285  0  0 100  0  0

注: 如果不清楚vmstat里各指标的含义,可以执行man vmstat查询

从输出中可以看到:

  • 内存的free列在不停的变化,并且是下降趋势
  • buffer和cache基本保持不变

未使用内存在逐渐减小而buffercache基本不变,这说明系统中使用的内存一直在升高
但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。比如:程序中如果用了一个动态增长的数组来缓存计算结果,占用内存自然会增长

2.4 确定是否内存泄漏

那怎么确定是不是内存泄漏呢?或者换句话说,有没有简单方法找出让内存增长的进程,并定位增长内存用在哪儿呢?

是否可以用topps来观察进程的内存使用情况,然后找出内存使用一直增长的进程,最后再通过pmap查看进程的内存分布

这种方法并不太好用,因为要判断内存的变化情况,需要写一个脚本来处理top或者ps的输出

2.5 检测内存泄漏工具memleak

这里,使用一个专门用来检测内存泄漏的工具memleak
memleak可以跟踪系统或指定进程的内存分配、释放请求,然后定期输出一个未释放内存和相应调用栈的汇总情况(默认5 秒)

memleakbcc软件包中的一个工具,执行/usr/share/bcc/tools/memleak就可以运行,运行下面的命令:

# ‑a 表示显示每个内存分配请求的大小以及地址
# ‑p 指定案例应用的PID号
$ /usr/share/bcc/tools/memleak ‑a ‑p $(pidof app)
WARNING: Couldn't find .text section in /app
WARNING: BCC can't handle sym look ups for /app
    addr = 7f8f704732b0 size = 8192
    addr = 7f8f704772d0 size = 8192
    addr = 7f8f704712a0 size = 8192
    addr = 7f8f704752c0 size = 8192
    32768 bytes in 4 allocations from stack
        [unknown] [app]
        [unknown] [app]
        start_thread+0xdb [libpthread‑2.27.so] 

从memleak的输出可以看到,案例应用在不停地分配内存,并且这些分配的地址没有被回收

2.6 memleak工具问题解决

这里有一个问题,Couldn’t find .text section in /app,所以调用栈不能正常输出,最后的调用栈部分只能看到[unknown]的标志

为什么会有这个错误呢?
实际上,这是由于案例应用运行在容器中导致的。memleak工具运行在容器之外,并不能直接访问进程路径/app

比方说,在终端中直接运行ls命令,发现这个路径的确不存在:

$ ls /app
ls: cannot access '/app': No such file or directory

解决这个问题最简单的方法,就是在容器外部构建相同路径的文件以及依赖库
这个案例只有一个二进制文件,所以只要把案例应用的二进制文件放到/app路径中,就可以修复这个问题

比如把app二进制文件从容器中复制出来,然后重新运行memleak工具,运行下面的命令:

$ docker cp app:/app /app
$ /usr/share/bcc/tools/memleak ‑p $(pidof app) ‑a
Attaching to pid 12512, Ctrl+C to quit.
[03:00:41] Top 10 stacks with outstanding allocations:
    addr = 7f8f70863220 size = 8192
    addr = 7f8f70861210 size = 8192
    addr = 7f8f7085b1e0 size = 8192
    addr = 7f8f7085f200 size = 8192
    addr = 7f8f7085d1f0 size = 8192
    40960 bytes in 5 allocations from stack
        fibonacci+0x1f [app]
        child+0x4f [app]
        start_thread+0xdb [libpthread‑2.27.so] 

终于看到了内存分配的调用栈,原来是fibonacci()函数分配的内存没释放

2.7 问题源码修复

定位了内存泄漏的来源,下一步自然就应该查看源码,想办法修复它。一起看案例应用的源代码app.c

$ docker exec app cat /app.c
...
long long *fibonacci(long long *n0, long long *n1)
{
    //分配1024个长整数空间方便观测内存的变化情况
    long long *v = (long long *) calloc(1024, sizeof(long long));
    *v = *n0 + *n1;
    return v;
}
void *child(void *arg)
{
    long long n0 = 0;
    long long n1 = 1;
    long long *v = NULL;
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\\n", n, *v);
        sleep(1);
    }
}
... 

发现child()调用fibonacci()函数,但并没有释放fibonacci()返回的内存。所以,想要修复泄漏问题,在child()中加一个释放函数即可,比如:

void *child(void *arg)
{
    ...
    for (int n = 2; n > 0; n++) {
        v = fibonacci(&n0, &n1);
        n0 = n1;
        n1 = *v;
        printf("%dth => %lld\\n", n, *v);
        free(v);    // 释放内存
        sleep(1);
    }
} 

2.8 问题修复验证

把修复后的代码放到了app-fix.c,然后打包成了一个Docker镜像

接下来验证一下内存泄漏是否修复,运行下面的命令:

# 清理原来的案例应用
$ docker rm ‑f app
# 运行修复后的应用
$ docker run ‑‑name=app ‑itd feisky/app:mem‑leak‑fix
# 重新执行 memleak工具检查内存泄漏情况
$ /usr/share/bcc/tools/memleak ‑a ‑p $(pidof app)
Attaching to pid 18808, Ctrl+C to quit.
[10:23:18] Top 10 stacks with outstanding allocations:
[10:23:23] Top 10 stacks with outstanding allocations:

可以看到案例应用已经没有遗留内存,证明修复工作成功完成

三、小结

应用程序可以访问的用户内存空间,由只读段、数据段、堆、栈以及文件映射段等组成
其中,堆内存和内存映射,需要应用程序来动态管理内存段。不仅要会用标准库函数malloc()来动态分配内存,还要记得在用完内存后调用库函数 _free()_释放它们

上面的案例相对比较简单,只加一个free()调用就能修复内存泄漏
不过,实际应用程序就复杂多了,比如:

  • malloc()free()通常并不是成对出现,而是需要在每个异常处理路径和成功路径上都释放内存
  • 在多线程程序中,一个线程中分配的内存可能会在另一个线程中访问和释放
  • 在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放

所以,为了避免内存泄漏,分配内存后一定要先写好内存释放的代码,再去开发其他逻辑

可以用memleak工具,检查应用程序的运行中内存是否泄漏。如果发现了内存泄漏情况,再根据memleak输出的应用程序调用栈定位内存的分配位置,从而释放不再访问的内存

以上是关于linux性能优化内存泄漏的定位和处理的主要内容,如果未能解决你的问题,请参考以下文章

SRS性能(CPU)、内存优化工具用法

Android 性能优化之内存泄漏检测以及内存优化(上)

android—性能优化2—内存优化

linux性能优化如何定位系统内存的问题

Android遇到内存泄漏和性能优化,需要采取以下措施

内存泄漏的定位与排查:Heap Profiling 原理解析