Linux内存泄漏排查与恢复

摘要:
为了协调CPU与磁盘间的性能差异,Linux还会使用Cache和Buffer,分别把文件和磁盘读写的数据缓存到内存中。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。如果应用程序没有正确释放堆内存,就会造成内存泄漏。内存泄漏不断累积,甚至会耗尽系统内存。但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大。专门用来检测内存泄漏的工具,memleak。

进程使用内存概念

对普通进程来说,能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。当进程通过 malloc() 申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访问时,才通过缺页异常陷入内核中分配内存。为了协调 CPU 与磁盘间的性能差异,Linux 还会使用 Cache 和 Buffer ,分别把文件和磁盘读写的数据缓存到内存中。对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。管理内存的过程中,也很容易发生各种各样的“事故”,比如,没正确回收分配后的内存,导致了泄漏。访问的是已分配内存边界外的地址,导致程序异常退出,等等。

内存的分配和回收,过程中造成内存泄漏的问题分析

进程的内存空间时,用户空间内存包括多个不同的内存段,比如只读段、数据段、堆、栈以及文件映射段等。这些内存段正是应用程序使用内存的基本方式。

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

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

很多时候,并不知道数据大小,所以就要用到标准库函数 malloc() _,_ 在程序中动态分配内存。这时候,系统就会从内存空间的堆中分配内存。堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。

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

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

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

内存泄漏的危害非常大

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

虽然,系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM 前,可能已经引发了一连串的反应,导致严重的性能问题。其他需要内存的进程,可能无法分配新的内存;内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等等。

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

斐波那契数列是一个这样的数列: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 CPU,8GB 内存预先安装 sysstat、Docker 以及 bcc 软件包,

sysstat 软件包中的 vmstat ,可以观察内存的变化情况;而 Docker 可以运行案例程序。

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

确认案例应用已经正常启动。如果一切正常,应该可以看到下面这个界面:

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

运行下面的 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 

从输出中可以看到,内存的 free 列在不停的变化,并且是下降趋势;而 buffer 和 cache 基本保持不变。

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

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

memleak 是 bcc 软件包中的一个工具,一开始就装好了,执行 /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 的输出可以看到,应用在不停地分配内存,并且这些分配的地址没有被回收。这里有一个问题,Couldn’t find .text section in /app,所以调用栈不能正常输出,最后的调用栈部分只能看到 [unknown] 的标志。实际上,这是由于案例应用运行在容器中导致的。memleak 工具运行在容器之外,并不能直接访问进程路径 /app.最简单的方法,就是在容器外部构建相同路径的文件以及依赖库。这个只有一个二进制文件,所以只要把应用的二进制文件放到 /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() 函数分配的内存没释放。定位了内存泄漏的来源,下一步自然就应该查看源码,想办法修复它

$ 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, *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, *v);
        free(v);    // 释放内存
        sleep(1);
    }
} 

修复后重新运行

# 清理原来的案例应用
$ 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() 通常并不是成对出现,在每个异常处理路径和成功路径上都释放内存 。在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。所以,为了避免内存泄漏,最重要的一点就是养成良好的编程习惯,比如分配内存后,一定要先写好内存释放的代码,再去开发其他逻辑。还是那句话,有借有还,才能高效运转,再借不难。当然,如果已经完成了开发任务,你还可以用 memleak 工具,检查应用程序的运行中,内存是否泄漏。如果发现了内存泄漏情况,再根据 memleak 输出的应用程序调用栈,定位内存的分配位置,从而释放不再访问的内存。

免责声明:文章转载自《Linux内存泄漏排查与恢复》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇QTP安装错误整理npm使用记录下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

软件开发行为规范-华为

软件开发行为规范   1 软件需求分析 2 软件项目计划 3 概要设计 4 详细设计 5 编码 6 需求管理 7 软件配置管理 8 软件质量保证 9 数据度量和分析   为了把公司已经发布的软件开发过程规范有效地运作于产品开发活动中,把各种规范“逐步形成工程师的作业规范”,特制定本软件开发行为规范,以达到过程控制的目的。 与软件开发相关的所有人员,包括各级...

Linux基础(Ubuntu16.04):安装vim及配置

1.进入终端  Ctrl + Alt +T 出现终端窗口 2.输入命令: sudo apt-get install vim-gtk 3.验证是否成功   安装完vim后查看命令 vi tab键,就会关联出所有vi开头的命令,看是否有vim,有则成功. 4.美化vim   输入命令:sudo vim /etc/vim/vimrc   注意: 必须加上sudo...

Linux下的sleep()和sched_yield()(转)

阿里四面被问到了这个问题,一脸懵逼,下来也没找到什么阐述这个的文章,就自己查man来对比总结一下吧: sched_yield()的man手册描述如下: DESCRIPTION       sched_yield()  causes  the  calling  thread to relinquish the CPU.  The  thread is mo...

【转】foxmail突然打不开了,双击没反应,怎么回事呀

原文网址:http://tieba.baidu.com/p/3492526384 解决方法如下:1、进入foxmail安装目录(默认在D盘Program Files下层,右击foxmail这个文件夹,去掉只读;2、确定后,尝试打开foxmail,如果依然打不开,那么再重复第1条,发现foxmail文件夹变成只读,又是只读,没错,你没看错,又被360杀毒软件...

安卓应用在各大应用市场上架方法整理

想要把APP上架到应用市场都要先注册开发者账号才可以。这里的方法包括注册帐号和后期上架及一些需要注意的问题。注意:首次提交应用绝对不能随便删除,否则后面再提交会显示应用APP冲突,会要求走应用认领流程,那个时候就会相当麻烦啦。 1、腾讯应用宝 腾讯开放平台地址:http://open.qq.com 注册开发者帐号地址:https://ssl.zc.qq.c...

Tess4J -4.0.2- Linux 实践 [解决:Tess4J

Tess4J是Tesseract的Java JNA wrapper。本文介绍了在CentOS 7 操作系统中使用Tess4J的步骤及注意事项。在正式开始之前,先花一点篇幅,对相关的技术作一简要介绍。 一点点背景 Tesseract Tesseract 是一个著名的开源OCR引擎,支持100多种语言,可以开箱即用。还可以通过训练方式支持更多语言。Tesser...