100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > linux 【内存】4. 内存泄漏了 我该如何定位和处理?

linux 【内存】4. 内存泄漏了 我该如何定位和处理?

时间:2020-01-18 01:19:54

相关推荐

linux 【内存】4. 内存泄漏了 我该如何定位和处理?

文章目录

1. 回顾2. 内存的分配和回收3. 案例4. 总结

1. 回顾

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

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

今天我就带你来看看,内存泄漏到底是怎么发生的,以及发生内存泄漏之后该如何排查和定位。说起内存泄漏,这就要先从内存的分配和回收说起了。

2. 内存的分配和回收

你还记得应用程序中,都有哪些方法来分配内存吗?用完后,又该怎么释放还给系统呢?

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

举个例子,你在程序中定义了一个局部变量,比如一个整数数组 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。

3. 案例

今天的案例基于 Ubuntu 18.04,当然,同样适用其他的 Linux 系统。

机器配置:2 CPU,8GB 内存

预先安装 sysstat、Docker 以及 bcc 软件包,比如:

# install sysstat dockersudo apt-get install -y sysstat docker.io# Install bccsudo apt-key adv --keyserver --recv-keys 4052245BD4284CDDecho "deb /apt/bionic bionic main" | sudo tee /etc/apt/sources.list.d/iovisor.listsudo apt-get updatesudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)

$ docker run --name=app -itd feisky/app:mem-leak$ docker logs app2th => 13th => 24th => 35th => 56th => 87th => 13

从输出中,我们可以发现,这个案例会输出斐波那契数列的一系列数值。实际上,这些数值每隔 1 秒输出一次。知道了这些,我们应该怎么检查内存情况,判断有没有泄漏发生呢?你首先想到的可能是 top 工具,不过,top 虽然能观察系统和进程的内存占用情况,但今天的案例并不适合。内存泄漏问题,我们更应该关注内存使用的变化趋势。

运行下面的 vmstat ,等待一段时间,观察内存的变化情况。如果忘了 vmstat 里各指标的含义,记得复习前面内容,或者执行 man vmstat 查询

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

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

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

那怎么确定是不是内存泄漏呢?或者换句话说,有没有简单方法找出让内存增长的进程,并定位增长内存用在哪儿呢?根据前面内容,你应该想到了用 top 或 ps 来观察进程的内存使用情况,然后找出内存使用一直增长的进程,最后再通过pmap查看进程的内存分布。

但这种方法并不太好用,因为要判断内存的变化情况,还需要你写一个脚本,来处理 top 或者 ps 的输出。这里,我介绍一个专门用来检测内存泄漏的工具,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 /appWARNING: BCC can't handle sym look ups for /appaddr = 7f8f704732b0 size = 8192addr = 7f8f704772d0 size = 8192addr = 7f8f704712a0 size = 8192addr = 7f8f704752c0 size = 819232768 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。

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

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

类似的问题,我在 CPU 模块中的 perf 使用方法中已经提到好几个解决思路。最简单的方法,就是在容器外部构建相同路径的文件以及依赖库。这个案例只有一个二进制文件,所以只要把案例应用的二进制文件放到 /app 路径中,就可以修复这个问题。

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

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

这一次,我们终于看到了内存分配的调用栈,原来是fibonacci()函数分配的内存没释放。定位了内存泄漏的来源,下一步自然就应该查看源码,想办法修复它。我们一起来看案例应用的源代码 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);}}

我把修复后的代码放到了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:

现在,我们看到,案例应用已经没有遗留内存,证明我们的修复工作成功完成。

4. 总结

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

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

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

所以,为了避免内存泄漏,最重要的一点就是养成良好的编程习惯,比如分配内存后,一定要先写好内存释放的代码,再去开发其他逻辑。还是那句话,有借有还,才能高效运转,再借不难。当然,如果已经完成了开发任务,你还可以用memleak工具,检查应用程序的运行中,内存是否泄漏。如果发现了内存泄漏情况,再根据 memleak 输出的应用程序调用栈,定位内存的分配位置,从而释放不再访问的内存。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。