作为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:显示选择任务的线程的统计信息外的额外信息 -TTASKCHILD | 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
....

CentOS 内存溢出后 linux内存溢出排查_CentOS 内存溢出后

这里面说明的挺详细的了,如果遇到内存问题,观察这个肯定会发现猫腻的。