Linux性能优化实战学习笔记:第十八讲

 1、内存的分配和回收

一、管理内存的过程当中,也很容易发生各类各样的“事故”,

对应用程序来讲,动态内存的分配和回收,是既核心又复杂的一的一个逻辑功能模块。管理内存的过程当中,也很容易发生各类各样的“事故”,linux

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

你在程序中定义了一个局部变量,好比一个整数数组 int data[64] ,就定义了一个能够存储 64 个整数的内存段。因为这是一个局部变量,它会从内它会从内存空间的栈中分配内存编程

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

 

二、 堆内存由应用程序本身来分配和管理。 除非程序退出,这些堆内存并不会被系统自动释放,而是须要应用程序明确调用库函数 free() 来释放它们。若是应用程序没有正确释放堆内存,数组

      就会形成内存泄漏。缓存

这是两个栈和堆的例子,那么,其余内存段是否也会致使内存泄漏呢?通过咱们前面的学习,这个问题并不难回答bash

二、只读段、只读段、只读段

只读段:包括程序的代码和常量,因为是只读的,不会再去分配新的的内存,因此也不会产生内存泄漏。多线程

数据段:包括全局变量和静态变量,这些变量在定义时就已经肯定了大小,因此也不会产生内存泄漏。app

最后一个内存映射段:包括动态连接库和共享内存,其中共享内存由程序动态分配和管理。因此,若是程序在分配后忘了回收,就会致使跟堆内存相似的泄漏问题。ionic

内存泄漏的危害很是大,这些忘记释放的内存,不只应用程序本身不能访问,系统也不能把他们再次分配给其余应用,内存泄露不断累积,甚至耗尽系统内存

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

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

内存泄漏的危害这么大,那咱们应该怎么检测这种问题呢?特别是,若是你已经发现了内存泄漏,该如何定位和处理呢。

接下来,咱们就用一个计算斐波那契数列的案例,

斐波那契数列是一个这样的数列:0、一、一、二、三、五、8…,也就是除了前两个数是 0 和1,其余数都由前面两数相加获得,用数学公式来表示就是 F(n)=F(n-1)+F(n-2),(n>=2),F(0)=0, F(1)=1

2、案例

一、环境准备

今天的案例基于 Ubuntu 18.04,固然,一样适用其余的 Linux 系统。
机器配置:2 CPU,8GB 内存
预先安装 sysstat、Docker 以及 bcc 软件包,好比:

# install sysstat docker
sudo apt-get install -y sysstat docker.io

# Install bcc
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
echo "deb https://repo.iovisor.org/apt/bionic bionic main" | sudo tee /etc/apt/sources.list.d/iovisor.list
sudo apt-get update
sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)

其中,sysstat 和 Docker 咱们已经很熟悉了。sysstat 软件包中的 vmstat ,能够观察内存的变化状况;而 Docker 能够运行案例程序。
bcc 软件包前面也介绍过,它提供了一系列的 Linux 性能分析工具,经常使用来动态追踪进程和内核的行为。更多工做原理你先不用深究,后面学习咱们会逐步接触。这里你只须要记
住,按照上面步骤安装完后,它提供的全部工具都位于 /usr/share/bcc/tools 这个目录中。

注意:bcc-tools 须要内核版本为 4.1 或者更高,若是你使用的是CentOS7,或者其余内核版本比较旧的系统,那么你须要手动升级内核版本后再安装。

二、服务运行环境

root@luoahong ~]# docker run --name=app -itd feisky/app:mem-leak
Unable to find image 'feisky/app:mem-leak' locally
mem-leak: Pulling from feisky/app
473ede7ed136: Pull complete
c46b5fa4d940: Pull complete
93ae3df89c92: Pull complete
6b1eed27cade: Pull complete
22dd80cda054: Pull complete
f7c1129fca8d: Pull complete
Digest: sha256:a6806d6b0f33aedc31a6e6c9bd77fe80a086b5c97869a25859e4894dec7b8d4b
Status: Downloaded newer image for feisky/app:mem-leak
b316f0fb07945bd283ffa2d4768440515ffbb01f607a8051a31fce3f9c0fb297

案例成功运行后,你须要输入下面的命令,确认案例应用已经正常启动。若是一切正常,你应该能够看到下面这个界面:

[root@luoahong ~]# docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
... ...
38th => 39088169
39th => 63245986
40th => 102334155

从输出中,咱们能够发现,这个案例会输出斐波那契数列的一系列数值。实际上,这些数值每隔 1 秒输出一次。

知道了这些,咱们应该怎么检查内存状况,判断有没有泄漏发生呢?你首先想到的多是top 工具,不过,top 虽然能观察系统和进程的内存占用状况,但今天的案例并不适合。
内存泄漏问题,咱们更应该关注内存使用的变化趋势。

三、发现问题

因此,开头我也提到了,今天推荐的是另外一个老熟人, vmstat 工具。运行下面的 vmstat ,等待一段时间,观察内存的变化状况。若是忘了 vmstat 里各指标的含义,记得复习前面内容,或者执行 man vmstat 查询。

[root@luoahong ~]# 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 7177068   2072 576796    0    0   466   187  257  365  1  3 90  5  0
 0  0      0 7177068   2072 576796    0    0     0     0  132  231  0  0 100  0  0
 0  0      0 7177068   2072 576796    0    0     0     0  138  234  0  0 100  0  0
 0  0      0 7176908   2072 576796    0    0     0     0  128  228  0  0 100  0  0
 0  0      0 7176908   2072 576800    0    0     0     0  129  225  0  0 100  0  0
 0  0      0 7176908   2072 576800    0    0     0     0  136  241  0  0 100  0  0
 1  0      0 7176968   2072 576800    0    0     0     6  166  257  0  1 99  0  0
 0  0      0 7176968   2072 576800    0    0     0     0  137  246  0  0 100  0  0
 0  0      0 7176968   2072 576800    0    0     0     0  132  237  0  0 100  0  0
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 7176744   2072 576800    0    0     0     0  138  236  0  1 99  0  0
 0  0      0 7176744   2072 576800    0    0     0     0  129  233  0  0 100  0  0
 0  0      0 7176744   2072 576800    0    0     0     0  127  240  0  0 100  0  0
 0  0      0 7176744   2072 576800    0    0     0     0  122  229  0  0 100  0  0
 0  0      0 7176680   2072 576800    0    0     0     0  131  241  0  0 100  0  0
 0  0      0 7176680   2072 576800    0    0     0     0  131  233  0  0 100  0  0
 0  0      0 7176680   2072 576800    0    0     0     0  145  244  1  0 99  0  0

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

未使用内存在逐渐减少,而 buffer 和 cache 基本不变,这说明,系统中使用的内存一直在升高。但这并不能说明有内存泄漏,由于应用程序运行中须要的内存也可能会增大。好比说,程序中若是用了一个动态增加的数组来缓存计算结果,占用内存天然会增加。

3、分析过程

那怎么肯定是否是内存泄漏呢?或者换句话说,有没有简单方法找出让内存增加的进程,并定位增加内存用在哪儿呢?

根据前面内容,你应该想到了用 top 或 ps 来观察进程的内存使用状况,而后找出内存使用一直增加的进程,最后再经过 pmap 查看进程的内存分布。

但这种方法并不太好用,由于要判断内存的变化状况,还须要你写一个脚本,来处理 top或者 ps 的输出。

一、memleak分析工具

这里,我介绍一个专门用来检测内存泄漏的工具,memleak。memleak 能够跟踪系统或指定进程的内存分配、释放请求,而后按期输出一个未释放内存和相应调用栈的汇总状况(默认 5 秒)。
固然,memleak 是 bcc 软件包中的一个工具,咱们一开始就装好了,执行/usr/share/bcc/tools/memleak 就能够运行它。好比,咱们运行下面的命令:

# -a 表示显示每一个内存分配请求的大小以及地址
# -p 指定案例应用的 PID 号
[root@luoahong ~]# /usr/share/bcc/tools/memleak -a -p $(pidof app)
WARNING: Couldn't find .text section in /app
WARNING: BCC can't handle sym look ups for /app
    addr = 7f8f704732b0 size = 8192
    addr = 7f8f704772d0 size = 8192
    addr = 7f8f704712a0 size = 8192
    addr = 7f8f704752c0 size = 8192
    32768 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问题解决

Couldn’t find .text section in /app,因此调用栈不能正常输出,最后的调用栈部分只能看到 [unknown] 的标志。
为何会有这个错误呢?实际上,这是因为案例应用运行在容器中致使的。memleak 工具运行在容器以外,并不能直接访问进程路径 /app。
比方说,在终端中直接运行 ls 命令,你会发现,这个路径的确不存在:

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

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

好比,你能够运行下面的命令,把 app 二进制文件从容器中复制出来,而后从新运行memleak 工具:

docker cp app:/app /app
[root@luoahong ~]#  /usr/share/bcc/tools/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() 函数分配的内存没释放

这一次,咱们终于看到了内存分配的调用栈,原来是 fibonacci() 函数分配的内存没释放。
定位了内存泄漏的来源,下一步天然就应该查看源码,想办法修复它。咱们一块儿来看案例应用的源代码 app.c:

[root@luoahong ~]# docker exec app cat /app.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

long long *fibonacci(long long *n0, long long *n1)
{
	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);
    }
} 

4、解决方案

我把修复后的代码放到了 app-fix.c,也打包成了一个 Docker 镜像。你能够运行下面的命令,验证一下内存泄漏是否修复:

# 清理原来的案例应用
[root@luoahong ~]# docker rm -f app
app

# 运行修复后的应用
[root@luoahong ~]# docker run --name=app -itd feisky/app:mem-leak-fix
Unable to find image 'feisky/app:mem-leak-fix' locally
mem-leak-fix: Pulling from feisky/app
473ede7ed136: Already exists
c46b5fa4d940: Already exists
93ae3df89c92: Already exists
6b1eed27cade: Already exists
4d87f2538251: Pull complete
f7c1129fca8d: Pull complete
Digest: sha256:61d1ce0944188fcddb0ee78a2db60365133009b23612f8048b79c0bbc85f7012
Status: Downloaded newer image for feisky/app:mem-leak-fix
c2071e234b83d078716d5d5aa664047a8afd2abf67d10ecc89f77db3d173fb16

# 从新执行 memleak 工具检查内存泄漏状况
[root@luoahong ~]#/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:

如今,咱们看到,案例应用已经没有遗留内存,证实咱们的修复工做成功完成。

 5、小结

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

今天的案例比较简单,只用加一个 free() 调用就能修复内存泄漏。不过,实际应用程序就复杂多了。好比说,
malloc() 和 free() 一般并非成对出现,而是须要你,在每一个异常处理路径和成功路径上都释放内存 。

在多线程程序中,一个线程中分配的内存,可能会在另外一个线程中访问和释放。更复杂的是,在第三方的库函数中,隐式分配的内存可能须要应用程序显式释放。

因此,为了不内存泄漏,最重要的一点就是养成良好的编程习惯,好比分配内存后,必定要先写好内存释放的代码,再去开发其余逻辑。仍是那句话,有借有还,才能高效运
转,再借不难。

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

相关文章
相关标签/搜索