普通进程通过内核提供的虚拟内存工作,虚拟内存需通过页表映射到物理内存。进程调用 malloc() 分配虚拟内存时,物理内存的分配仅在首次访问触发缺页异常时进行。Linux 使用 Cache 和 Buffer 缓存文件及磁盘数据,以优化 CPU 与磁盘间的性能差异。动态内存的分配与回收是应用程序中的核心逻辑,但也容易引发问题,如内存泄漏和越界访问导致程序异常等。
回顾内存分配和回收,进程的内存空间包括栈、堆、只读段、数据段和内存映射段等。
- 栈:局部变量从栈分配,内存由系统自动管理,作用域结束后自动回收,无内存泄漏风险。
- 堆:动态内存从堆分配,需要程序手动管理并释放,未释放可能导致内存泄漏。
- 只读段:存储代码和常量,不会动态分配,无内存泄漏风险。
- 数据段:包括全局和静态变量,大小固定,无内存泄漏风险。
- 内存映射段:动态链接库和共享内存,其中共享内存需手动管理,可能导致内存泄漏。
1. 案例
环境准备:
- 操作系统:Ubuntu 18.04
- 机器配置: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)
注意:bcc-tools 需要内核版本为 4.1 或者更高,如果你使用的是 CentOS7,或者其他内核版本比较旧的系统,那么你需要手动升级内核版本后再安装。
运行案例
$ docker run --name=app -itd feisky/app:mem-leak
执行案例程序,看到如下输出说明执行正常。
$ docker logs app
2th => 1
3th => 2
4th => 3
5th => 5
6th => 8
7th => 13
另外一个终端执行
# 每隔3秒输出一组数据
$ 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
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 6601824 97620 1098784 0 0 0 0 62 322 0 0 100 0 0
0 0 0 6601700 97620 1098788 0 0 0 0 57 251 0 0 100 0 0
0 0 0 6601320 97620 1098788 0 0 0 3 52 306 0 0 100 0 0
0 0 0 6601452 97628 1098788 0 0 0 27 63 326 0 0 100 0 0
2 0 0 6601328 97628 1098788 0 0 0 44 52 299 0 0 100 0 0
0 0 0 6601080 97628 1098792 0 0 0 0 56 285 0 0 100 0 0
free一直在下降。此时无法判断是否存在内存泄露,因为程序使用内存是正常行为,需要借助工具memleak。
# -a 表示显示每个内存分配请求的大小以及地址
# -p 指定案例应用的PID号
$ docker cp app:/app /app
$ /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函数并没有释放内存
正常输出应如上。
修改后的程序:
# 清理原来的案例应用
$ docker rm -f app
# 运行修复后的应用
$ docker run --name=app -itd feisky/app:mem-leak-fix
# 重新执行 memleak工具检查内存泄漏情况
$ /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:
当然,在生产实际环境中内存泄露的排查并没有如此简单。