正常情况下,软件使用的内存是随着时间在一定范围内波动的。内存泄漏说的是随着时间的推移,进程使用的内存越来越多。一般情况下,内存泄漏是程序不断地申请内存,但是没有释放导致的。
1 linux 中内存信息查看
要定位内存泄漏问题,首先要知道怎么查看内存使用信息,包括系统级别的内存使用,进程级别的内存使用情况。本节记录几个查看内存信息的命令。
1.1 系统级别
1.1.1 free
free 是我们经常使用的命令,显示信息如下图所示,默认单位是 KB。
free 命令显示了两行信息,第一行是机器物理内存的使用情况,第二行是 swap 分区的使用情况。
swap 分区的使用情况,一共 3 个数,比较好理解,total 是总大小,used 是已使用的大小,free 是空闲。
内存各项的统计说明如下:
total | 物理内存总量 |
used | 真正在使用的内存,这些内存现在在使用,不能回收 |
free | 真正空闲的内存,在 buddy 中管理 |
shared | 共享内存 |
buff/cache | 文件系统页缓存,dentry, inode 缓存 |
available | 可以使用的内存 |
total = used + free + buff/cache
3964428 = 904688 + 1389096 + 1670644
used 是真正在使用,不能回收; free 是真正空闲,被伙伴系统管理着;buff/cache 是介于 used 和 free 之外的中间态。total 等于 3 者之和。
shared 是被多个进程共享的内存,是正在被使用,不能回收的内存。
available 是综合 free 和 buff/cache 的结果,表示当前可以使用的内存,包括全部的 free 以及 buff/cache 中可以回收的内存。
释放缓存:
linux 中有一个文件 /proc/sys/vm/drop_caches,向这个文件中写 1 会释放页缓存,写 2 会释放 dentry 和 inode 缓存,写 3 会释放页缓存以及 dentry 和 inode 缓存。
1.1.2 free 与 /proc/buddyinfo 的关系
在 linux 相关的书籍中经常看到一句话 "页是内存管理的基本单位"。一开始看到这句话的时候,自己感到疑惑,一个页的大小是 4KB,而我们经常使用的内存,比如一个结构体,大小也就是几十个字节,我们申请一个 1 字节的内存也是可以申请出来的。内存管理的基本单位难道不是字节吗,为什么是页呢。
页是内存管理的基本单位,说的是内存管理中的伙伴系统。系统启动之后,一开始内存都是按页被伙伴系统管理的,在伙伴系统中,内存以页为单位进行管理。buddy 是最底层的内存管理的系统,内核中和用户态使用的内存都要从 buddy 中申请。
buddy 中有 11 个链表,第一个链表中的一项是 1 个内存连续的页;第二个链表中的一项是 2 个内存连续的页;第三个链表中的一项是 4 个内存连续的页;以此类推,最后一个链表中的一项是 1024 个内存连续的页。
free 命令中的 free 统计表示还在伙伴系统中的内存
cat /proc/buddinfo 显示的伙伴系统的信息。
buddy 显示的是空闲页的信息,第一列的页数是 2 的 0 次方乘以 464(18 + 466),第二列的页数是 2 的 1 次方乘以 215(8 + 207),以此类推,最后一列的页数是 2 的 10 次方乘以 328(2 + 324 + 1)。
总空闲页数是:
1 * 484,2 * 215,4 * 60,8 * 37,16 * 34,32 * 14,64 * 14,128 * 11,256 * 8,512 * 9,1024 * 328。
总的空闲页数是上边数据的和,为 347274,一个页的大小是 4KB,所以总的空闲内存是 1389096KB,与 free 中显示的 free 是相等的。
1.2 进程级别
1.2.1 /proc/pid/status
每个进程都会在 /proc 下创建一个文件夹,文件夹的名字是进程的 pid,在这个文件夹下有进程的所有信息。
status 包括进程的一些状态信息,比如 pid, ppid,进程发生的调度次数,进程的内存使用情况等。
如下几项,都是对内存的统计:
VmPeak | 进程所占用最大虚拟内存大小 |
VmSize | 进程当前虚拟内存大小 |
VmLck | 被锁定的内存大小 |
VmHWM | 进程所占用物理内存的峰值 |
VmRSS | 进程当前占用物理内存的大小 |
VmData | 进程数据段的大小 |
VmStk | 进程堆栈段的大小 |
VmExe | 进程代码的大小 |
VmLib | 进程所使用共享库的大小 |
其中 VmRSS 是实际占用的内存,在定位内存泄漏问题是,一般需要持续观察 VmRSS,是不是随着时间一直在增长。
1.2.2 smem
smem 可以查看系统里边所有进程的内存使用情况。
如下图所示,smem 统计项包括 4 个:Swap,USS,PSS, RSS。
RSS: 实际使用的物理内存,包括共享内存
PSS: 实际使用的物理内存,共享内存是按比例统计,假如一个共享动态库占用了 500KB 的内存,这个库被 5 个进程共享,那么统计到一个进程中的内存就是 100KB。而在 RSS 统计中是每个进程都统计了 500KB
USS: 进程独占的物理内存,不包括共享内存
由此可知,RSS, PSS, USS 的大小关系:USS <= PSS <= RSS
smem 命令可以按某一项进行排序,如图中所示按 pss 进行排序,默认是从小到大排序,-r 是翻过来,从大到小排序。
1.2.3 top
top 命令非常常用,可以查看 cpu 信息,内存信息,平均负载信息等。
top 命令显示的信息如下,可以看到平均负载,每个 cpu 的使用信息,内存的使用情况,swap 的使用情况。另外还能看到每个进程的 cpu 和内存使用情况。
2 应用内存泄漏定位
2.1 asan
编译的时候带上标志 gcc -fsanitize=address memleak.c,这样当内存越界的时候或者内存泄漏的时候就能检查出来。
内存泄漏:
示例代码如下,在函数 mem_leadk() 中通过 malloc() 申请了 10 字节的内存,然后没有释放,这样在进程退出的时候就可以检查出来存在内存泄漏。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void mem_leak() {
char *p = (char *)malloc(10);
p[5] = 5;
}
int main() {
mem_leak();
sleep(5);
return 0;
}
打印信息如下图所示,打印出了泄漏的内存申请时的栈以及泄漏的内存大小等信息。
内存越界:
示例代码如下,在函数 array_exceeded() 中定义了一个长度为 10 的数组,在访问的时候时候 a[10] 构造数组越界的情况。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void array_exceeded() {
int a[10];
a[0] = 1;
a[10] = 2;
}
int main() {
array_exceeded();
sleep(5);
return 0;
}
打印信息如下,打印了数组越界位置的调用栈等信息。
在工作中还用过另一个 gcc 内置的工具(可见 gcc 的功能还是很强大的),用于统计代码覆盖率的工具,coverage。如下代码,编译的时候带上 -coverage 选项,就会生成 一个 .gcno 文件,运行程序的时候会生成 .gcda 文件。最后使用 gcov .gcda 会生成 .gcov 文件。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void say_hello() {
printf("hello\n");
}
int main() {
say_hello();
return 0;
}
执行 gcov .gcda 生成 .gcov 文件的时候就会打印出来覆盖率信息。
.gcov 文件中的内容显示了每行代码被执行的次数。
2.2 valgrind
valgrind 也可以用于检测内存泄漏。
valgrind 和 asan 两种方式对比如下:
(1)valgrind 的优点是应用不用重新编译,直接就能运行,缺点是需要专门安装 valgrind
(2)asan 的优点是 gcc 自带的,不用借助于第三方工具
缺点是需要单独编译,带有 asan 的版本只能作为 debug
ubuntu 安装 valgrind:
apt-get install valgrind
使用 valgrind:
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./a.out
最重要的一个选项是 --leak-check=full
valgrind 打印的信息如下,打印信息中包括泄漏的内存信息,以及申请内存的调用栈。
2.3 用户态内存泄漏占用内存太多会被 SIGKILL 杀掉
进程占用内存太多的时候会 oom 被系统杀死,杀死进程的信号是 SIGKILL。
如下是一个狂占内存的代码,可以运行观察一下。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main() {
while (1) {
char *p = (char *)malloc(4096);
if (p) {
p[0] = 10;
p[1000] = 10;
}
}
return 0;
}
系统杀死进程的时候会打印一条内核日志,使用 dmesg 可以看到。
3 内核内存泄漏
内核中使用的内存绝大多数都是从 slab 中申请的。当内核发生内存泄漏的时候,可以查看 slab 的信息,确定哪个池子的内存一直在增长,确定哪个池子在增长就可以进行下一步的定位工作。
/proc/slabinfo 可以查看 slab 内存的使用情况。
也可以使用 slabtop 命令查看 slab 内存的使用情况。
对于内核开发者来说,可以直接修改内核代码来进行定位。
而如果修改编译内核代码不方便的话,可以使用 linux tracepoint 中的 kmem 相关的 tracepoint 来进行跟踪定位。
举个例子,假如我们通过查看 slab 信息发现有一个内存池占用的内存一直在增长,内存池中的 buffer 大小是 4096。我们就可以使用如下的命令打开 kmalloc 的 tracepoint,如下命令当 kmalloc 申请的内存是 4096 时打印调用栈。基于调用栈信息,我们可以向下进一步分析。
echo 'stacktrace if bytes_req == 4096' > /sys/kernel/debug/tracing/events/kmem/kmalloc/trigger