如何定位和处理内存泄漏

本文介绍了内存泄漏的概念及其危害,重点讲述了如何在Linux环境中检测和处理内存泄漏问题。通过一个计算斐波那契数列的案例,演示了使用vmstat和memleak工具进行内存监控和定位内存泄漏源的过程,最后强调了动态内存管理的重要性,特别是在多线程和使用第三方库时需要注意的内存释放问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于普通进程来说,能看到的其实是内核提供的虚拟内存,这些虚拟内存通过页表,由系统映射为物理内存。
当进程通过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 13 …,也就是除了前两项,后面的数都是前面两数相加的值,用公式表达就是F(n)=F(n-1)+F(n-2),(n>=2).F(1)=1.F(0)=0。

案例

配置

Ubuntu 18.04
2CPU 8GB
预先安装sysstat、Docker以及bcc软件包

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

root@VM-4-9-ubuntu:~# docker run --name=app -itd feisky/app:mem-leak

案例成功运行之后,需要输入下面的命令,确认案例应用已经正常启动。如果一切正常,就可以看到下面这个界面

root@VM-4-9-ubuntu:~# docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13
8th => 21
9th => 34
......

从输出中,我们可以发现,这个案例会输出斐波那契数列的一系列数值。实际上,这些数值每隔一秒输出一次。
知道了这些,我们要怎么检查内存情况,判断有没有泄漏发生,在今天的案例中,用到的查看工具的是vmstat工具。
运行一段时间,观察内存的变化情况。如果不知道每项的意思,可以使用 man stat查看

root@VM-4-9-ubuntu:~# 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
 2  0      0 186740 160692 2733832    0    0     1    13   38   39  1  0 99  0  0
 0  0      0 187756 160692 2733856    0    0     0     3  857 1658  1  1 99  0  0
 0  0      0 187764 160692 2733876    0    0     0    23  925 1806  1  0 99  0  0
 0  0      0 186188 160692 2733892    0    0     0    32  840 1560  1  2 98  0  0
 0  0      0 185660 160692 2733900    0    0     0     0  694 1319  1  0 99  0  0
 0  0      0 187748 160692 2733900    0    0     0   207  867 1629  0  1 99  0  0
 0  0      0 187520 160692 2733904    0    0     0     0  775 1524  0  0 99  0  0
 0  0      0 187488 160692 2733908    0    0     0    11  840 1608  1  1 98  0  0
 0  0      0 187488 160692 2733908    0    0     0    17  635 1242  0  0 99  0  0
 0  0      0 186488 160692 2733908    0    0     0     0  678 1294  0  1 99  0  0
 0  0      0 187252 160692 2734044    0    0     0    12  856 1649  1  1 98  0  0
 0  0      0 187252 160692 2734052    0    0     0     0  954 1851  1  1 99  0  0
 0  0      0 186920 160692 2734052    0    0     0    17  713 1375  1  0 99  0  0
 0  0      0 186672 160692 2734056    0    0     0     9  743 1430  1  1 99  0  0

从输出可以看到,内存的free在不停的变化,差不多就是下降趋势,而buffer和cache基本保持不变。

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

那要怎么确定是不是内存泄漏,或者换句话说,找出让内存增长的进程,并定位问题

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

当然,memleak是bcc软件包中的一个工具,我们一开始就装好了,执行/usr/share/bcc/tools/memleak就可以运行它。比如,我们运行下面的命令:

# -a 表示显示每个内存分配请求的大小以及地址
# -p 指定案例应用的PID号
root@VM-4-9-ubuntu:~# /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的输出可以看到,案例应用在不停地分配内存,并且这些分配的地址没有被回收。这里只有一个问题,can’t handle sym look ups for /app,所以调用栈不能正常输出,最后只能看到的是unknown的标志。之所以有个错误,是因为案例应用运行在容器中导致的。memleak工具运行在容器之外,并不能直接访问进程路径/app。
所以,运行下面命令,把app二进制文件从容器中复制出来,然后重新运行。


root@VM-4-9-ubuntu:~# docker cp app:/app /app
root@VM-4-9-ubuntu:~# /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()函数分配的内存没释放。定位了内存泄漏的来源,下一步自然就是查看源码,想办法修复它。

root@VM-4-9-ubuntu:~# docker exec app cat /app.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

long long *fibonacci(long long *n0, long long *n1)
{
        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);
        }
}


int main(void)
{
        pthread_t tid;
        pthread_create(&tid, NULL, child, NULL);
        pthread_join(tid, NULL);
        printf("main thread exit\n");
        return 0;
}root@VM-4-9-ubuntu:~#

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

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;
                free(v);	//释放内存
                printf("%dth => %lld\n", n, *v);
                sleep(1);
        }
}

修复的代码放到了app-fix.c,打包成一个Docker镜像,从新运行下实例。

root@VM-4-9-ubuntu:~# docker rm -f app
app
root@VM-4-9-ubuntu:~# docker run --name=app -itd feisky/app:mem-leak-fix
#重新运行下memleak
root@VM-4-9-ubuntu:~# /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()通常不是成对出现,而是需要在每个异常处理路径和成功路径上都释放内存。
  • 在多线程程序中,一个线程中分配的内存,可能会在另一个线程中访问和释放。
  • 更复杂的是,在第三方的库函数中,隐式分配的内存可能需要应用程序显式释放。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值