Linux内核主要学习内容可以分为三大块:进程、内存及协议栈。
今天就说说内存泄露的问题。
类似问题,很可能就是内存泄漏导致的。我们都知道,内存泄漏指的是内存被分配出去后一直没有被释放,导致这部分内存无法被再次使用,甚至更加严重的是,指向这块内存空间的指针都不存在了,进而再也无法访问这块内存空间。
我们平时遇到的内存泄漏可能是应用程序的内存泄漏,也可能是内核(操作系统)的内存泄漏;而应用程序的内存泄漏可能是堆内存(heap)的泄漏,也可能是内存映射区(Memory Mapping Region)的泄漏。这些不同类型的内存泄漏,它们的表现形式也是不一样的,解决方案也不一样,所以为了更好地处理内存泄漏问题,我们首先就需要去了解这些不同的内存类型。
这些不同的内存类型都可以理解为是进程地址空间 (Address Space) 的一部分,那地址空间是怎么工作的呢?
我们用一张图,来表示进程的地址空间。图的左侧是说进程可以通过什么方式来更改进程虚拟地址空间,而中间就是进程虚拟地址空间是如何划分的,右侧则是进程的虚拟地址空间所对应的物理内存或者说物理地址空间。
我们来具体聊一下这个过程。
应用程序首先会调用内存申请释放相关的函数,比如 glibc 提供的 malloc(3)、 free(3)、calloc(3) 等;或者是直接使用系统调用 mmap(2)、munmap(2)、 brk(2)、sbrk(2) 等。
如果使用的是库函数,这些库函数其实最终也是对系统调用的封装,所以可以理解为是应用程序动态申请释放内存,最终是要经过 mmap(2)、munmap(2)、brk(2)、sbrk(2) 等这些系统调用。当然从库函数到系统调用,这其中还涉及到这些库本身进行的一些内存层面的优化,比如说,malloc(3) 既可能调用 mmap(2),又可能会调用 brk(2)。
然后这些内存申请和释放相关的系统调用会修改进程的地址空间 (address space),其中 brk(2) 和 sbrk(2) 修改的是 heap(堆),而 mmap(2) 和 munmap(2) 修改的是 Memory Mapping Region(内存映射区)。
请注意这些针对的都是虚拟地址,应用程序都是跟虚拟地址打交道,不会直接跟物理地址打交道。而虚拟地址最终都要转换为物理地址,由于 Linux 都是使用 Page(页)来进行管理的,所以这个过程叫 Paging(分页)。
我们用一张表格来简单汇总下这些不同的申请方式所对应的不同内存类型,这张表格也包含了我们在课程上一个模块讲的 Page Cache,所以你可以把它理解为是进程申请内存的类型大汇总:
这里面涉及很多术语,我们对其中重要的部分做些简单介绍,来看看这张表里的哪些部分容易出现内存泄漏。
进程运行所需要的内存类型有很多种,总的来说,这些内存类型可以从是不是文件映射,以及是不是私有内存这两个不同的维度来做区分,也就是可以划分为上面所列的四类内存。
了解了进程虚拟地址空间这些不同的内存类型后,我们来继续看下它们对应的物理内存。
刚刚我们也提到,进程虚拟地址空间是通过 Paging(分页)这种方式来映射为物理内存的,进程调用 malloc() 或者 mmap() 来申请的内存都是虚拟内存,只有往这些内存中写入数据后(比如通过 memset),才会真正地分配物理内存 。
你可能会有疑问,如果进程只是调用 malloc() 或者 mmap() 而不去写这些地址,即不去给它分配物理内存,是不是就不用担心内存泄漏了?
答案是这依然需要关注内存泄露,因为这可能导致进程虚拟地址空间耗尽,即虚拟地址空间同样存在内存泄露的问题。
接下来,我们继续用一张图片来细化一下分页的过程。
如上图所示,Paging 的大致过程是,CPU 将要请求的虚拟地址传给 MMU(Memory Management Unit,内存管理单元),然后 MMU 先在高速缓存 TLB(Translation Lookaside Buffer,页表缓存)中查找转换关系,如果找到了相应的物理地址则直接访问;如果找不到则在地址转换表(Page Table)里查找计算。最终进程访问的虚拟地址就对应到了实际的物理地址。
了解了地址空间的相关知识之后,你就能够对进程的地址空间做一个合理的规划,或者说合理的控制了。这样出现问题时,不至于产生太严重的影响,你可以把规划好进程的地址空间理解为是进程内存问题的兜底方案。Linux 上最典型的规划进程地址空间的方式就是通过 ulimit,你可以通过调配它,来规划进程最大的虚拟地址空间、物理地址空间、栈空间是多少,等等。
对于进程地址空间相关的知识我们先聊到这里,接下来我们看下如何使用工具来观察进程的地址空间。
学会观察进程地址空间是分析内存泄漏问题的前提,当你怀疑内存有泄漏时,首先需要去观察哪些内存在持续增长,哪些内存特别大,这样才能够判断出内存泄漏大致是出在哪里,然后针对性地去做分析;相反,如果你在没有仔细观察进程地址空间之前,就盲目猜测问题出在哪,处理问题很可能会浪费大量时间,甚至会南辕北辙。
那么都有哪些观察进程的工具呢?我们常用来观察进程内存的工具,比如说 pmap、ps、top 等,都可以很好地来观察进程的内存。
首先我们可以使用 top 来观察系统所有进程的内存使用概况,打开 top 后,然后按 g 再输入 3,从而进入内存模式就可以了。在内存模式中,我们可以看到各个进程内存的 %MEM、VIRT、RES、CODE、DATA、SHR、nMaj、nDRT,这些信息通过 strace 来跟踪 top 进程,你会发现这些信息都是从 /proc/[pid]/statm 和 /proc/[pid]/stat 这个文件里面读取的:
$ strace -p `pidof top` open("/proc/16348/statm", O_RDONLY) = 9 read(9, "40509 1143 956 24 0 324 0\n", 1024) = 26 close(9) = 0 ... open("/proc/16366/stat", O_RDONLY) = 9 read(9, "16366 (kworker/u16:1-events_unbo"..., 1024) = 182 close(9) ...
除了 nMaj(Major Page Fault, 主缺页中断,指内容不在内存中然后从磁盘中来读取的页数)外,%MEM 则是从 RES 计算而来的,其余的内存信息都是从 statm 文件里面读取的,如下是 top 命令中的字段和 statm 中字段的对应关系:
另外如果你观察仔细的话,可能会发现,有些时候所有进程的 RES 相加起来要比系统总的物理内存大,这是因为 RES 中有一些内存是被一些进程给共享的。
在明白了系统中各个进程的内存使用概况后,如果想要继续看某个进程的内存使用细节,你可以使用 pmap。如下是 pmap 来展示 sshd 进程地址空间里的部分内容:
$ pmap -x `pidof sshd` Address Kbytes RSS Dirty Mode Mapping 000055e798e1d000 768 652 0 r-x-- sshd 000055e7990dc000 16 16 16 r---- sshd 000055e7990e0000 4 4 4 rw--- sshd 000055e7990e1000 40 40 40 rw--- [ anon ] ... 00007f189613a000 1800 1624 0 r-x-- libc-2.17.so 00007f18962fc000 2048 0 0 ----- libc-2.17.so 00007f18964fc000 16 16 16 r---- libc-2.17.so 00007f1896500000 8 8 8 rw--- libc-2.17.so ... 00007ffd9d30f000 132 40 40 rw--- [ stack ] ...
每一行表示一种类型的内存(Virtual Memory Area),每一列的含义如下。
可以看到,通过 pmap 我们能够清楚地观察一个进程的整个的地址空间,包括它们分配的物理内存大小,这非常有助于我们对进程的内存使用概况做一个大致的判断。比如说,如果地址空间中[heap]太大,那有可能是堆内存产生了泄漏;再比如说,如果进程地址空间包含太多的 vma(可以把 maps 中的每一行理解为一个 vma),那很可能是应用程序调用了很多 mmap 而没有 munmap;再比如持续观察地址空间的变化,如果发现某些项在持续增长,那很可能是那里存在问题。
pmap 同样也是解析的 /proc 里的文件,具体文件是 /proc/[pid]/maps 和 /proc/[pid]/smaps,其中 smaps 文件相比 maps 的内容更详细,可以理解为是对 maps 的一个扩展。你可以对比 /proc/[pid]/maps 和 pmaps 的输出,你会发现二者的内容是一致的。
除了观察进程自身的内存外,我们还可以观察进程分配的内存和系统指标的关联,我们就以常用的 /proc/meminfo 为例,来说明我们上面提到的四种内存类型(私有匿名,私有文件,共享匿名,共享文件)是如何体现在系统指标中的。
如上图所示,凡是私有的内存都会体现在 /proc/meminfo 中的 AnonPages 这一项,凡是共享的内存都会体现在 Cached 这一项,匿名共享的则还会体现在 Shmem 这一项。
$ cat /proc/meminfo ... Cached: 3799380 kB ... AnonPages: 1060684 kB ... Shmem: 8724 kB ...
同样地,我也建议你动手写一些测试用例来观察下,这样你理解得就会更深刻。
我们对进程的内存管理相关的基础知识就先讲到这里。
关于Linux内核的学习技术路线:
Linux学习视频资料: