4张图教你高效解决Linux内存泄漏

4张图教你高效解决Linux内存泄漏

目录

大家好,这里是物联网心球。一直以来,笔者都想写一篇Linux内存泄漏相关的文章,原因有两点:内存泄漏是软件开发中经常会碰到的问题,并且很多初学并不会排查Linux内存泄漏问题;笔者工作的这些年里,掌握了很多内存泄漏调试的方法,这些方法比较零散,借着这篇文章整理一下零碎的知识,形成一篇Linux内存调试指南,帮助大家解决Linux内存泄漏问题。

1.从Linux内存管理开始

关于Linux内存管理机制我的很多篇文章都有过介绍。为什么要反复介绍这个知识点,是因为它非常重要,只要涉及到Linux内存相关的问题,我们都需要借助Linux内存管理机制来解决。

4张图教你高效解决Linux内存泄漏 图1

图1 Linux内存管理

如图1所示,Linux内存管理分为:虚拟内存管理和物理内存管理。关于这一点,我的这边文章有过介绍:一张图搞懂mmap实现原理(通俗易懂),相同的知识点就不再重复介绍。 本文我们从内存泄漏的角度来探讨Linux内存管理。Linux内存管理的核心包括:虚拟内存(对应虚拟地址)、页表机制、物理内存(对应物理地址)。其中,物理内存是稀缺资源,虚拟内存相对来说没有什么限制。用户程序申请内存时,系统首先会分配虚拟内存给用户,只有用户需要真正使用内存时才会分配物理内存。也就是说虚拟内存映射至物理内存有一定的滞后性。这种滞后性是Linux系统的一种内存安全保护机制,如果每个进程分配内存时立即给他分配物理内存,那么物理内存会很快被消耗完。 由于内存映射的滞后性,所以我们在排查内存错误时,也要考虑这一点。当发生内存错误时,我们要同时查看虚拟内存和物理内存使用情况。

2.系统物理内存

物理内存属于系统级别资源,系统上的进程和线程共享物理内存。任何一个进程或线程发生物理内存泄漏时,都会造成物理内存资源越来越少。所以,我们需要经常观察物理内存的健康状况。

4张图教你高效解决Linux内存泄漏 图2

图2 查看系统物理内存

如图2所示,常见的查看系统物理内存方式有:

  • free命令
  • top命令
  • cat /proc/meminfo

其实这三种方式并没有本质区别,它们的共同点都是去读取/proc/meminfo文件,所以我们需要深入理解/proc/meminfo文件才能够了解系统物理内存使用情况。 /proc/meminfo文件提供了系统内存使用情况的详细信息,这些信息由内核实时生成,因此可以反映当前系统的物理内存状态。相关输出示例如下:

4张图教你高效解决Linux内存泄漏 图3

由于meminfo文件输出信息比较多,并且部分信息我们很少用到且初学者很难理解,我们只讲解一些关键字段,如下:

  • MemTotal: 总物理内存大小。
  • MemFree: 空闲物理内存(未被使用)大小。
  • MemAvailable: 估计可用于新应用程序的物理内存(包含缓存和缓冲)大小。
  • Buffers: 用于文件系统缓冲区的物理内存(临时存储磁盘数据)大小。
  • Cached: 用于页面缓存的物理内存(缓存文件数据)大小。

Linux系统将物理内存划分为一个个小的内存单元(page,通常为4KB),内核通过struct page结构来描述page。由于page的数量很多,Linux通过伙伴系统来统一管理这些page。当我们想要了解系统物理内存使用情况时,只需要统计伙伴系统中所有的page使用情况即可。对伙伴系统感兴趣的同学可以查看我的这篇文章:图解Linux内存管理_伙伴系统。 用户程序读取/proc/meminfo文件其实就是查看伙伴系统中page的使用情况。

3.进程虚拟内存

很多读者一直搞不清楚虚拟内存和物理内存,物理内存属于系统级别,虚拟内存则属于进程级别。每个进程都有独立虚拟内存(对应虚拟地址空间),并且虚拟地址范围是相同的。不同进程可以使用相同的虚拟地址来访问内存,虽然使用的是相同的虚拟地址,但是系统会通过页表机制,将相同的虚拟地址映射至不同的物理地址,从而访问不同的物理内存。

4张图教你高效解决Linux内存泄漏 图4

图3 查看进程虚拟内存

如图3所示,当我们想要查看某个进程虚拟内存使用情况时,通常有以下几种方法(<pid>表示进程ID):

  • 查看虚拟内存整体状况:cat /proc/<pid>/status。
  • 查看虚拟内存各个内存区域状态:pmap -x <pid>或cat /proc/<pid>/smaps。

/proc/<pid>/status文件提供了指定进程的详细状态信息。这些信息由内核实时生成,可以反映当前进程的运行状态、资源使用情况等。输出示例如下:

4张图教你高效解决Linux内存泄漏 图5

常见字段介绍如下:

  • VmPeak:进程虚拟内存使用峰值,包括所有已分配的内存,无论是否使用。

  • VmSize:进程当前虚拟内存总量(含代码、数据、堆栈、共享库等所有映射区域)。

  • VmHWM:进程物理内存使用峰值(历史最大RSS,反映物理内存压力)。

  • VmRSS:进程当前物理内存使用量。

  • RssAnon:进程匿名映射物理内存(不属于任何文件的内存,通常是程序的堆和栈)使用量。

  • RssFile:进程文件映射物理内存(共享库、mmap 文件等)使用量。

  • RssShmem:进程共享内存物理使用量。

  • VmData:数据段大小(包括动态分配的内存和全局变量)。

  • VmStk:栈大小。

  • VmExe:代码段大小。

  • VmLib:共享库虚拟内存大小。

接下来,我们来看一下进程虚拟内存各个区域的使用情况,由于pmap方式的底层依赖/proc/<pid>/smaps文件,所以我们重点讲解/proc/<pid>/smaps文件。

/proc/<pid>/smaps文件提供了指定进程的内存映射的详细信息。这些信息包括每个内存段的大小、访问权限、使用情况等。

相关输出示例如下:

4张图教你高效解决Linux内存泄漏 图6

由于smaps文件的输出信息比较多,我们只截取了heap(堆区)来进行讲解。 559fc50000-559fc71000 rw-p 00000000 00:00 0 [heap]

  • 559fc50000-559fc71000:表示该内存区域的起始和结束地址。
  • rw-p:表示该内存区域的访问权限,rw表示可读写,p表示私有(private)。
  • 00000000:表示该内存区域在文件中的偏移量。
  • 00:00 0:表示该内存区域没有关联的文件,00:00是设备号,0是文件的inode号。
  • [heap]:表示该内存区域是进程的堆(heap)。

常见字段解析如下:

  • Size:该内存区域虚拟内存总大小。

  • Rss:该内存区域当前物理内存使用量。

  • Pss:该内存区域按共享比例计算的物理内存使用量。

4.内存分配实战

前面介绍了很多Linux内存管理相关的理论知识,为了让大家有更直观的了解,我们通过测试来验证这些理论知识,测试程序如下:

#define SIZE (100 * 1024 * 1024)

int main(int argc, char *argv[]) {
    void *p = malloc(SIZE); //申请100MB内存
    while(1) sleep(1);

    return 0;
}

测试程序调用malloc函数申请100MB内存并且不进行释放,编译测试程序并执行,分别查看程序虚拟内存和物理内存使用情况。执行cat /proc/<pid>/status命令查看虚拟内存整体状态,输出结果如下:

# cat /proc/2604694/status
......
VmPeak: 104596 kB
VmSize: 104596 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 640 kB
VmRSS: 640 kB
RssAnon: 0 kB
RssFile: 640 kB
RssShmem: 0 kB
VmData: 102616 kB
VmStk: 132 kB
VmExe: 4 kB
VmLib: 1740 kB
VmPTE: 36 kB
VmSwap: 0 kB

VmPeak(进程虚拟内存使用峰值)和VmSize(进程虚拟内存当前使用量)都为100MB,说明系统已经为进程分配了100MB虚拟内存。我们再通过cat /proc/<pid>/smaps命令查看各个虚拟内存区域内存使用情况,输出结果如下:

5597713000-5597734000 rw-p 0000000000:000           [heap]
Size: 132 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 4 kB
Pss: 4 kB
Pss_Dirty: 4 kB
......
VmFlags: rd wr mr mw me ac
7f7973f000-7f7fb40000 rw-p 0000000000:000
Size: 102404 kB //通过mmap匿名映射分配100MB内存
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 4 kB
Pss: 4 kB
......
VmFlags: rd wr mr mw me ac

从输出结果可以看出来,这100MB内存是通过mmap匿名映射方式分配。尽管是调用malloc函数分配内存,但是实际是通过mmap匿名映射分配内存。为什么会这样呢?请查看我的这篇文章:14张图彻底搞懂malloc、free源码。 查看完虚拟内存使用情况后,我们还需要查看物理内存使用情况。我们通过cat /proc/meminfo命令查看物理内存使用情况。 未启动测试程序输出结果如下:

# cat /proc/meminfo
MemTotal: 3882916 kB
MemFree: 1521360 kB
MemAvailable: 3554568 kB
Buffers: 130392 kB
Cached: 1837244 kB

启动测试程序输出结果如下:

# cat /proc/meminfo
MemTotal: 3882916 kB
MemFree: 1524100 kB
MemAvailable: 3557308 kB
Buffers: 130392 kB
Cached: 1837244 kB

两次输出结果MemFree(空闲物理内存)相差不大,说明测试程序并没有分配物理内存。没有分配物理内存的原因是因为我们并没有实际使用内存。接下来,我们稍微修改一下测试程序,如下:

#define SIZE (100 * 1024 * 1024)

int main(int argc, char *argv[]) {
    void *p = malloc(SIZE); //申请100MB内存
    memset(p, 0, SIZE); //完成内存映射,使用物理内存
    while(1) sleep(1);

    return 0;
}

测试程序通过memset将申请的内存初始化,编译测试程序并执行。我们再通过cat /proc/<pid>/smaps命令查看各个虚拟内存区域内存使用情况,输出结果如下:

55ac9a1000-55ac9c2000 rw-p 0000000000:000           [heap]
Size: 132 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 4 kB
Pss: 4 kB
......
VmFlags: rd wr mr mw me ac
7fb731f000-7fbd720000 rw-p 0000000000:000
Size: 102404 kB //通过mmap分配100MB虚拟内存
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 102404 kB //分配物理内存100MB
Pss: 102404 kB //分配物理内存100MB
......
VmFlags: rd wr mr mw me ac

Size为102404KB表示分配100MB虚拟内存。Rss和Pss为102404KB表示虚拟地址已经完成内存映射,该区域实际物理内存使用量为100MB。最后,我们再来看一下系统物理内存的变化,未启动测试程序执行cat /proc/<pid>/smaps命令的输出结果如下:

# cat /proc/meminfo
MemTotal: 3882916 kB
MemFree: 1522124 kB
MemAvailable: 3555336 kB
Buffers: 130392 kB
Cached: 1837244 kB

启动测试程序输出结果如下:

# cat /proc/meminfo
MemTotal: 3882916 kB
MemFree: 1420504 kB
MemAvailable: 3453716 kB
Buffers: 130392 kB
Cached: 1837244 kB

两次结果MemFree相差100MB,说明系统已经为测试程序分配100MB物理内存。

5.内存泄漏排查

在Linux系统中,当用户程序在使用malloc、calloc或realloc等函数分配内存后,忘记调用free函数来释放内存并会造成内存泄漏。

4张图教你高效解决Linux内存泄漏 图7

图4 Linux内存泄漏原理

Linux内存泄漏原理如图4所示,当用户程序调用内存分配函数(如:malloc函数)分配内存时,malloc函数最终将从堆区或者通过mmap匿名内存映射区分配内存。实际上,堆区也是mmap匿名映射的一种。从图4可知,内存泄漏的区域主要是堆区和匿名映射区,所以排查内存泄漏的思路也将聚焦在这两个内存区域。 Linux内存泄漏排查步骤如下。(1)确认内存泄漏的存在

在开始排查之前,需要先确认系统确实存在内存泄漏。排查方法可查看/proc/meminfo文件。(2)确定内存泄漏的进程

要确定哪个进程造成的内存泄漏,我们需要按照内存使用量对Linux进程进行排序,相关命令如下:

ps -eo pid,comm,rsz,vsz --sort=-rsz | head -n 10

rsz表示物理内存,vsz表示虚拟内存。–sort=-rsz表示按实际物理内存使用量从高到低排序,如果想按虚拟内存使用量排序,可将rsz改为vsz。输出示例如下:

# ps -eo pid,comm,rsz,vsz --sort=-rsz | head -n 10
    PID COMMAND RSZ VSZ
2638056 a.out 103040 104596
    958 pcmanfm 64412 541472
    956 wf-panel-pi 29380 1200136
    711 labwc 18256 575252
    253 systemd-journal 15056 66944
2518507 cups-browsed 12032 178884
2527597 vim 9984 15940
2518581 sshd 9856 19652
2518583 sshd 9856 19656

从输出结果可知,测试程序a.out共使用100MB物理内存,按照物理内存排序,排名第一。

(3)分析进程的内存使用情况

确定内存泄漏进程后,我们可以通过cat /proc/<pid>/status和at /proc/<pid>/smaps进一步详细查看进程虚拟内存和物理内存使用情况。

(4)确定代码内存泄漏点

完成以上步骤后,我们对程序是否存在内存泄漏已经有了全面的了解。如果程序存在内存泄漏,那么我们需要找到内存泄漏点在代码哪个位置。有经验的开发者通常会采用工具分析和代码审查的方式来查找内存泄漏点。内存泄漏分析工具有很多,其中比较知名的有valgrind。

使用valgrind之前需要先安装,Ubuntu安装方法如下:

sudo apt-get install valgrind

安装完毕后,使用valgrind启动你的程序,方法如下:

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --log-file=valgrind.txt ./myprogram
  • --leak-check=full:启用详细的内存泄漏检查。

  • --show-leak-kinds=all:显示所有类型的内存泄漏

  • --track-origins=yes:跟踪未初始化值的来源。

  • --verbose:启用详细模式,输出更多调试信息。

  • --log-file=valgrind.txt:将输出结果保存到指定的日志文件中。

valgrind输出结果如下:

==2657638== LEAK SUMMARY:
==2657638== definitely lost: 0 bytes in 0 blocks
==2657638== indirectly lost: 0 bytes in 0 blocks
==2657638== possibly lost: 104,857,600 bytes in 1 blocks
==2657638== still reachable: 0 bytes in 0 blocks
==2657638== suppressed: 0 bytes in 0 blocks
==2657638==
==2657638== For lists of detected and suppressed errors, rerun with: -s
==2657638== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
  • definitely lost:程序分配的内存块完全无法访问,且未被释放,必须修复。
  • indirectly lost:内存因其他 definitely lost的内存块导致无法访问(如数据结构中父节点泄漏导致子节点泄漏),修复关联的definitely lost后会自动消失,无需单独处理。
  • possibly lost:存在指针指向内存块,但指针位于内存块中间而非起始地址(如指针运算错误),需要修复。
  • still reachable:程序结束时,全局/静态变量仍持有内存指针 (如未释放的全局缓存),建议清理。
  • suppressed:错误被Valgrind预设规则或用户配置过滤,非实际泄漏,无需关注。
← 返回文章列表