18-案例篇:内存泄漏了,我该如何定位和处理?





内存的分配和回收

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

举个例子,在程序中定义了一个局部变量,比如一个整数数组int data[64] ,就定义了一个可以存储64个整数的内存段。
由于这是一个局部变量,它会从内存空间的栈中分配内存。

栈内存由系统自动分配和管理。
一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。

再比如,很多时候,事先并不知道数据大小,所以就要用到标准库函数malloc() 在程序中动态分配内存。
这时候,系统就会从内存空间的堆中分配内存。

堆内存由应用程序自己来分配和管理。
除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数free()来释放它们。
如果应用程序没有正确释放堆内存,就会造成内存泄漏。

其他内存段是否也会导致内存泄漏呢?

  1. 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
  2. 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
  3. 内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。
    所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能 把它们再次分配给其他应用。
内存泄漏不断累积,甚至会耗尽系统内存。

虽然,系统最终可以通过OOM(Out of Memory)机制杀死进程,但进程在OOM前,可能已经引发了一连串的反应,导致严重的性能问题。

比如,其他需要内存的进程,可能无法分配新的内存;
内存不足,又会触发系统的缓存回收以及SWAP机制,从而进一步导致I/O的性能问题等等。




案例

  1. 实验环境

    # 用一个计算斐波那契数列的案例,来看看内存泄漏问题的定位和处理方法
    
    # 安装bcc-tools包,需要4.x的内核版本才行
    [root@VM-0-11-centos ~]# uname -r
    4.18.0-305.3.1.el8.x86_64
    
    # centos7.6版本 参考这篇博客:https://www.cnblogs.com/lichengguo/p/15668561.html
    # centos8.2的内核版本符合要求
    [root@VM-0-11-centos ~]# cat /etc/redhat-release
    CentOS Linux release 8.2.2004 (Core)
    [root@VM-0-11-centos ~]# python3
    Python 3.6.8 (default, Mar 19 2021, 05:13:41)
    [GCC 8.4.1 20200928 (Red Hat 8.4.1-1)] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>>
    
    # bcc-tools 需要内核版本为 4.1 或者更高,
    # 如果使用的是CentOS7或者其他内核版本比较旧的系统,那么你需要手动升级内核版本后再安装
    # python要3.0以上的版本(这个带验证)
    [root@VM-0-11-centos ~]# yum install docker sysstat bcc-tools -y
    
    # 配置环境变量
    [root@VM-0-11-centos ~]# vi /etc/profile
    export PATH=$PATH:/usr/share/bcc/tools
    [root@VM-0-11-centos ~]# source /etc/profile
    
    # 测试
    [root@VM-0-11-centos ~]# cachestat 1 3
        HITS   MISSES  DIRTIES HITRATIO   BUFFERS_MB  CACHED_MB
           0        0        0    0.00%            2        261
           0        0        0    0.00%            2        261
           0        0        0    0.00%            2        261
    
    


  1. 执行下面的命令来运行案例

    [root@VM-0-11-centos ~]# docker run --name=app -itd feisky/app:mem-leak
    
    

  2. 如果一切正常, 可以看到下面这个界面

    [root@VM-0-11-centos ~]# docker logs app
    2th => 1
    3th => 2
    4th => 3
    5th => 5
    6th => 8
    7th => 13
    
    

  3. 运行vmstat ,等待一段时间,观察内存的变化情况

    # 每隔 3 秒输出一组数据
    [root@VM-0-11-centos ~]# 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 2087792  55384 1404140    0    0   603  2679  510  671 11  2 86  1  0
     0  0      0 2087512  55392 1404168    0    0     0    29  342  591  0  0 100  0  0
     0  0      0 2085504  55400 1404172    0    0     0    24  526  908  1  1 99  0  0
     0  0      0 2086528  55400 1404172    0    0     0     0  800 1363  0  0 99  0  0
     0  0      0 2086544  55408 1404176    0    0     0     7  358  642  0  0 99  0  0
     0  0      0 2086528  55408 1404184    0    0     0     0  384  664  0  0 100  0  0
     0  0      0 2084116  55416 1404184    0    0     0   136  306  557  0  0 100  0  0
     0  0      0 2083960  55416 1404184    0    0     0     0  315  565  0  0 100  0  0
     0  0      0 2083872  55424 1404192    0    0     0    33  382  653  0  0 99  0  0
     0  0      0 2083896  55432 1404192    0    0     0    19  281  526  0  0 100  0  0
     0  0      0 2083800  55432 1404192    0    0     0     1  309  558  0  0 100  0  0
     0  0      0 2083780  55440 1404200    0    0     0     9  356  617  0  0 100  0  0
     2  0      0 2083768  55440 1404200    0    0     0     0  309  552  0  0 100  0  0
     0  0      0 2083772  55448 1404204    0    0     0    17  366  642  0  0 100  0  0
     0  0      0 1918536  55456 1404232    0    0     7    11  557  639  8  2 91  0  0
     0  0      0 1918316  55456 1404240    0    0     0     0  405  668  0  0 100  0  0
    
    从输出中可以看到,内存的free列在不停的变化,并且是下降趋势;而buffer和cache基本保持不变
    
    未使用内存在逐渐减小,而buffer和cache基本不变
    这说明,系统中使用的内存一直在升高
    但这并不能说明有内存泄漏,因为应用程序运行中需要的内存也可能会增大
    比如说,程序中如果用了一个动态增长的数组来缓存计算结果,占用内存自然会增长
    
    那怎么确定是不是内存泄漏呢?或者换句话说,有没有简单方法找出让内存增长的进程,并定位增长内存用在哪儿呢?
    memleak工具可以跟踪系统或指定进程的内存分配、释放请求
    然后定期输出一个未释放内存和相应调用栈的汇总情况(默认 5 秒)
    
    

  4. memleak是bcc软件包中的一个工具

    # -a 表示显示每个内存分配请求的大小以及地址
    # -p 指定案例应用的 PID 号
    [root@VM-0-11-centos ~]# memleak -a -p $(pidof app)
    [14:43:01] Top 10 stacks with outstanding allocations:
    	addr = 7f51780752d0 size = 8192
    	addr = 7f51780732c0 size = 8192
    	addr = 7f517806f2a0 size = 8192
    	addr = 7f51780772e0 size = 8192
    	addr = 7f51780712b0 size = 8192
    	40960 bytes in 5 allocations from stack
    		fibonacci+0x1f [app]
    		child+0x4f [app]
    		start_thread+0xdb [libpthread-2.27.so]
    
    [14:43:06] Top 10 stacks with outstanding allocations:
    	addr = 7f51780752d0 size = 8192
    	addr = 7f517807b300 size = 8192
    	addr = 7f51780732c0 size = 8192
    	addr = 7f517807f320 size = 8192
    	addr = 7f51780792f0 size = 8192
    	addr = 7f517806f2a0 size = 8192
    	addr = 7f517807d310 size = 8192
    	addr = 7f51780772e0 size = 8192
    	addr = 7f51780712b0 size = 8192
    	addr = 7f5178081330 size = 8192
    	81920 bytes in 10 allocations from stack
    		fibonacci+0x1f [app]
    		child+0x4f [app]
    		start_thread+0xdb [libpthread-2.27.so]
    		
    		
    从memleak的输出可以看到,案例应用在不停地分配内存,并且这些分配的地址没有被回收
    看到了内存分配的调用栈,原来是fibonacci()函数分配的内存没释放
    
    
    ############################################################
    如果fibonacci+0x1f [app] 这里显示的是[unknown] [app]
    这是由于案例应用运行在容器中导致的
    memleak工具运行在容器之外,并不能直接访问进程路径/app
    比方说,在终端中直接运行ls命令,这个路径的确不存在
    [root@VM-0-11-centos ~]# ls /app
    ls: cannot access '/app': No such file or directory
    类似的问题,在CPU模块中的perf使用方法中已经提到好几个解决思路
    最简单的方法,就是在容器外部构建相同路径的文件以及依赖库
    这个案例只有一个二进制文件,所以只要把案例应用的二进制文件放到/app路径中,就可以修复这个问题
    
    比如,可以运行下面的命令,把app二进制文件从容器中复制出来,然后重新运行memleak工具
    [root@VM-0-11-centos ~]# docker cp app:/app /app
    [root@VM-0-11-centos ~]# 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()函数分配的内存没释放
    ############################################################
    
    

  5. 查看源代码

    root@VM-0-11-centos ~]# 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);
    	}
    }
    
    
    int main(void)
    {
    	pthread_t tid;
    	pthread_create(&tid, NULL, child, NULL);
    	pthread_join(tid, NULL);
    	printf("main thread exit\n");
    	return 0;
    
    ##
    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);
        }
    } 
    
    

  6. 修复

    # 清理原来的案例应用
    [root@VM-0-11-centos ~]# docker rm -f app
    
    # 运行修复后的应用
    [root@VM-0-11-centos ~]#  docker run --name=app -itd feisky/app:mem-leak-fix
    
    # 重新执行 memleak 工具检查内存泄漏情况
    [root@VM-0-11-centos ~]# memleak -a -p `pidof app`
    ......
    Attaching to pid 14601, Ctrl+C to quit.
    [15:05:43] Top 10 stacks with outstanding allocations:
    [15:05:48] Top 10 stacks with outstanding allocations:
    
    可以看到,案例应用已经没有遗留内存,证明修复工作成功完成
    
    


小结

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

如果已经完成了开发任务,还可以用memleak工具,检查应用程序的运行中, 内存是否泄漏。
如果发现了内存泄漏情况,再根据memleak输出的应用程序调用栈,定位内存的分配位置,从而释放不再访问的内存。


posted @ 2021-12-09 15:40  李成果  阅读(277)  评论(0编辑  收藏  举报