Linux下内存泄漏排查
作为c的程序员,最常见的就是排查内存泄漏,不过我们一般的内存泄漏是针对特定的程序去排查,相对来说比较容易,但是如果是维护人员,不知道哪个程序有内存泄漏,甚至是应用程序的内存泄漏,还是内核的内存泄漏都不明确,所以一定要有一定的查内存泄漏的章法.
一 虚拟内存泄露
一般来说,我们观察系统的内存占用喜欢用top命令,然后输入m,对系统中整体的内存占用情况做个排序,然后在重点观察,内存占用排在前几位的进程,再逐步的分析,
[root@VM-0-2-centos ~]# top -p 5576
top - 18:21:46 up 198 days, 20:07, 2 users, load average: 0.10, 0.04, 0.05
Tasks: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.3 sy, 0.0 ni, 99.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1882008 total, 78532 free, 116516 used, 1686960 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1606660 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5576 root 20 0 184064 11248 1124 S 0.0 0.6 10:34.98 nginx
虽然top 也可以观察到单独的进程的内存变化,不过一般不太好比较内存变化的规律,
推荐使用pidstat工具,此工具需要先安装,通过命令:
yum install sysstat
pidstat 基本说明如下:
-u:默认的参数,显示各个进程的cpu使用统计
-r:显示各个进程的内存使用统计
-d:显示各个进程的IO使用情况
-p:指定进程号
-w:显示每个进程的上下文切换情况
-t:显示选择任务的线程的统计信息外的额外信息
-T { TASK | CHILD | ALL }
假如我们观察到如下的内存占用情况:pidstat -r -p pid 5
[root@VM-0-2-centos ~]# pidstat -r -p 5981 5
Linux 3.10.0-1127.19.1.el7.x86_64 (VM-0-2-centos) 07/24/2021 _x86_64_ (1 CPU)
06:25:55 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
06:26:00 PM 0 5981 0.20 0.00 4416 352 0.02 a.out
06:26:05 PM 0 5981 0.00 0.00 4416 352 0.02 a.out
06:26:10 PM 0 5981 0.20 0.00 4456 352 0.02 a.out
06:26:15 PM 0 5981 0.00 0.00 4456 352 0.02 a.out
06:26:20 PM 0 5981 0.00 0.00 4456 352 0.02 a.out
06:26:25 PM 0 5981 0.20 0.00 4496 352 0.02 a.out
06:26:30 PM 0 5981 0.00 0.00 4496 352 0.02 a.out
06:26:35 PM 0 5981 0.20 0.00 4536 352 0.02 a.out
06:26:40 PM 0 5981 0.00 0.00 4536 352 0.02 a.out
06:26:45 PM 0 5981 0.20 0.00 4576 352 0.02 a.out
06:26:50 PM 0 5981 0.00 0.00 4576 352 0.02 a.out
06:26:55 PM 0 5981 0.20 0.00 4616 352 0.02 a.out
我们注意下,VSZ即虚拟内存的占用每10s增加40,单位为k,即10s增加40k的虚拟内存。
下面来具体分析下这个程序的内存泄露情况。
二 分析泄露原因
我们来分析这个进程的内存分布情况,来分析这泄露的内存有什么特点:
[root@VM-0-2-centos ~]# pmap -x 5981
5981: ./a.out
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r-x-- a.out
0000000000600000 4 4 4 r---- a.out
0000000000601000 4 4 4 rw--- a.out
00007faab436e000 2720 272 272 rw--- [ anon ]
00007faab4616000 1804 260 0 r-x-- libc-2.17.so
00007faab47d9000 2048 0 0 ----- libc-2.17.so
00007faab49d9000 16 16 16 r---- libc-2.17.so
00007faab49dd000 8 8 8 rw--- libc-2.17.so
00007faab49df000 20 12 12 rw--- [ anon ]
00007faab49e4000 136 108 0 r-x-- ld-2.17.so
00007faab4a06000 2012 212 212 rw--- [ anon ]
00007faab4c03000 8 8 8 rw--- [ anon ]
00007faab4c05000 4 4 4 r---- ld-2.17.so
00007faab4c06000 4 4 4 rw--- ld-2.17.so
00007faab4c07000 4 4 4 rw--- [ anon ]
00007ffe0f3f5000 132 16 16 rw--- [ stack ]
00007ffe0f47c000 8 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
---------------- ------- ------- -------
total kB 8940 940 564
其中Address为开始的地址,Kbytes是虚拟内存的大小,RSS为真实内存的大小,Dirty为未同步到磁盘上的脏页,Mode为内存的权限,rw为可写可读,rx为可读和可执行。
通过几次观察,我们发现:
00007faab436e000 2720 272 272 rw--- [ anon ]
为泄露部分,此为匿名内存区,也就是没有映射文件,为malloc或mmap分配的内存。
同样是每次增加40K。
此时还是只能大概知道内存泄露的位置,我们还先找到具体的代码位置,这个该怎么分析?
代码的申请,无非是通过malloc和brk这些库函数进行内存调用,我们可以用strace跟踪下。
[root@VM-0-2-centos ~]# strace -f -t -p 5981 -o trace.strace
strace: Process 5981 attached
strace: Process 8519 attached
strace: Process 8533 attached
strace: Process 8547 attached
strace: Process 8557 attached
strace: Process 8575 attached
^Cstrace: Process 5981 detached
我们通过-t选项来显示时间,-f来跟踪子进程。直接用cat命令查看跟踪的文件内容,会发现内容相当多,只要是系统调用都打印了出来,可以通过每次增加40k这个有用的信息搜索下:
[root@VM-0-2-centos ~]# grep 40960 trace.strace
5981 19:01:44 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab403a000
5981 19:01:55 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4030000
5981 19:02:06 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4026000
5981 19:02:17 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab401c000
5981 19:02:28 mmap(NULL, 40960, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7faab4012000
至此我们找到了具体的泄露代码位置。
看下这个测试代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#define _SCHED_H
#define __USE_GNU
#include <bits/sched.h>
#define STACK_SIZE 40960
int func(void *arg)
{
printf("thread enter.\n");
sleep(1);
printf("thread exit.\n");
return 0;
}
int main()
{
int thread_pid;
int status;
int w;
while (1) {
void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);
if (addr == NULL) {
perror("mmap");
goto error;
}
printf("creat new thread...\n");
thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES, NULL);
printf("Done! Thread pid: %d\n", thread_pid);
if (thread_pid != -1) {
do {
w = waitpid(-1, NULL, __WCLONE | __WALL);
if (w == -1) {
perror("waitpid");
goto error;
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}
sleep(10);
}
error:
return 0;
}
这个测试程序利用mmap申请一块匿名私有的内存,clone为系统函数,pthread_create 和fork底层都是调用它,用来创建进程/线程,将func的地址指针存放在子进程堆栈的某个位置处,该位置就是该封装函数本身返回地址存放的位置,最后一个参数为func的执行参数。clone可以更灵活控制共享,比如可以控制是否共享内存空间,是否共享打开文件,是否共享相同的信号处理函数等。
我们可以看到,mmap申请内存后,需要通过munmap来释放,这里面没有释放,所以导致了虚拟内存泄露,这里面申请的内存只实际使用了4个字节,即复制了func的指针,其他的内存均没有使用,其实仔细观察会发现还有部分的物理内存泄露,每次4个字节,可以通过pmap -x 查到。
在 waitpid后面添加:munmap(addr,STACK_SIZE);
即可以实现内存的释放。
三 valgrind 分析程序内存泄露
这个是比较常见的方法,一般通过下面命令来查看内存泄露:
valgrind --tool=memcheck --leak-check=full ./b
[root@VM-0-2-centos test]# valgrind --tool=memcheck --leak-check=full ./b
==14374== Memcheck, a memory error detector
==14374== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==14374== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==14374== Command: ./b
==14374==
==14374== Warning: set address range perms: large range [0x5205040, 0x24605040) (undefined)
address:0x5205040
==14374== Warning: set address range perms: large range [0x5205040, 0x24605040) (defined)
524288000
==14374==
==14374== HEAP SUMMARY:
==14374== in use at exit: 524,288,000 bytes in 1 blocks
==14374== total heap usage: 1 allocs, 0 frees, 524,288,000 bytes allocated
==14374==
==14374== 524,288,000 bytes in 1 blocks are possibly lost in loss record 1 of 1
==14374== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==14374== by 0x400675: main (test.c:17)
==14374==
==14374== LEAK SUMMARY:
==14374== definitely lost: 0 bytes in 0 blocks
==14374== indirectly lost: 0 bytes in 0 blocks
==14374== possibly lost: 524,288,000 bytes in 1 blocks
==14374== still reachable: 0 bytes in 0 blocks
==14374== suppressed: 0 bytes in 0 blocks
明确说明在test.c的17行有可能是内存泄露:==14374== by 0x400675: main (test.c:17)
其他用法看说明吧。
四 其他内存泄露分析
其实上面的内存泄露是我们知道了具体的泄露的进程,然后再做详细分析。那么如果不知道哪里内存泄露了,有什么办法,可以通过分析meminfo文件,来观察泄露的类型。
[root@VM-0-2-centos test]# cat /proc/meminfo
MemTotal: 1882008 kB
MemFree: 752948 kB
MemAvailable: 1610108 kB
Buffers: 564900 kB
Cached: 399584 kB
SwapCached: 0 kB
Active: 808140 kB
Inactive: 220812 kB
Active(anon): 64548 kB
Inactive(anon): 488 kB
Active(file): 743592 kB
Inactive(file): 220324 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
....
图片摘抄自《linux内存技术实战课》
这里面说明的挺详细的了,如果遇到内存问题,观察这个肯定会发现猫腻的。
五 诗词欣赏
蝶恋花.春景
苏轼
花褪残红青杏小。燕子飞时,绿水人家绕。
枝上柳绵吹又少。天涯何处无芳草。
墙里秋千墙外道。墙外行人,墙里佳人笑。
笑渐不闻声渐悄。多情却被无情恼。