mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
160
极客时间专栏/Linux内核技术实战课/内存泄漏问题/06 基础篇 | 进程的哪些内存类型容易引起内存泄漏?.md
Normal file
160
极客时间专栏/Linux内核技术实战课/内存泄漏问题/06 基础篇 | 进程的哪些内存类型容易引起内存泄漏?.md
Normal file
@@ -0,0 +1,160 @@
|
||||
<audio id="audio" title="06 基础篇 | 进程的哪些内存类型容易引起内存泄漏?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/87/cf/874c294a0e6b8a8129ccf4276852a6cf.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。今天我们进入课程的第二个模块,来聊一下内存泄漏的话题。
|
||||
|
||||
相信你在平时的工作中,应该遇到过下面这些场景:
|
||||
|
||||
- 伴随着服务器中的后台任务持续地运行,系统中可用内存越来越少;
|
||||
- 应用程序正在运行时忽然被OOM kill掉了;
|
||||
- 进程看起来没有消耗多少内存,但是系统内存就是不够用了;
|
||||
- ……
|
||||
|
||||
类似问题,很可能就是内存泄漏导致的。我们都知道,内存泄漏指的是内存被分配出去后一直没有被释放,导致这部分内存无法被再次使用,甚至更加严重的是,指向这块内存空间的指针都不存在了,进而再也无法访问这块内存空间。
|
||||
|
||||
我们平时遇到的内存泄漏可能是应用程序的内存泄漏,也可能是内核(操作系统)的内存泄漏;而应用程序的内存泄漏可能是堆内存(heap)的泄漏,也可能是内存映射区(Memory Mapping Region)的泄漏。这些不同类型的内存泄漏,它们的表现形式也是不一样的,解决方案也不一样,所以为了更好地处理内存泄漏问题,我们首先就需要去了解这些不同的内存类型。
|
||||
|
||||
这些不同的内存类型都可以理解为是进程地址空间(Address Space)的一部分,那地址空间是怎么工作的呢?
|
||||
|
||||
## 进程的地址空间
|
||||
|
||||
我们用一张图,来表示进程的地址空间。图的左侧是说进程可以通过什么方式来更改进程虚拟地址空间,而中间就是进程虚拟地址空间是如何划分的,右侧则是进程的虚拟地址空间所对应的物理内存或者说物理地址空间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/32/c321c56a7b719bf14b0b5133d0a66132.jpg" alt="">
|
||||
|
||||
我们来具体聊一下这个过程。
|
||||
|
||||
应用程序首先会调用内存申请释放相关的函数,比如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,所以你可以把它理解为是进程申请内存的类型大汇总:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/0f/85e7da0e15587c6a1d31f7e60e1ab00f.jpg" alt="">
|
||||
|
||||
这里面涉及很多术语,我们对其中重要的部分做些简单介绍,来看看这张表里的哪些部分容易出现内存泄漏。
|
||||
|
||||
进程运行所需要的内存类型有很多种,总的来说,这些内存类型可以从是不是文件映射,以及是不是私有内存这两个不同的维度来做区分,也就是可以划分为上面所列的四类内存。
|
||||
|
||||
- **私有匿名内存**。进程的堆、栈,以及mmap(MAP_ANON | MAP_PRIVATE)这种方式申请的内存都属于这种类型的内存。其中栈是由操作系统来进行管理的,应用程序无需关注它的申请和释放;堆和私有匿名映射则是由应用程序(程序员)来进行管理的,它们的申请和释放都是由应用程序来负责的,所以它们是容易产生内存泄漏的地方。
|
||||
- **共享匿名内存**。进程通过mmap(MAP_ANON | MAP_SHARED)这种方式来申请的内存,比如说tmpfs和shm。这个类型的内存也是由应用程序来进行管理的,所以也可能会发生内存泄漏。
|
||||
- **私有文件映射**。进程通过mmap(MAP_FILE | MAP_PRIVATE)这种方式来申请的内存,比如进程将共享库(Shared libraries)和可执行文件的代码段(Text Segment)映射到自己的地址空间就是通过这种方式。对于共享库和可执行文件的代码段的映射,这是通过操作系统来进行管理的,应用程序无需关注它们的申请和释放。而应用程序直接通过mmap(MAP_FILE | MAP_PRIVATE)来申请的内存则是需要应用程序自己来进行管理,这也是可能会发生内存泄漏的地方。
|
||||
- **共享文件映射**。进程通过mmap(MAP_FILE | MAP_SHARED)这种方式来申请的内存,我们在上一个模块课程中讲到的File Page Cache就属于这类内存。这部分内存也需要应用程序来申请和释放,所以也存在内存泄漏的可能性。
|
||||
|
||||
了解了进程虚拟地址空间这些不同的内存类型后,我们来继续看下它们对应的物理内存。
|
||||
|
||||
刚刚我们也提到,进程虚拟地址空间是通过Paging(分页)这种方式来映射为物理内存的,进程调用malloc()或者mmap()来申请的内存都是虚拟内存,只有往这些内存中写入数据后(比如通过memset),才会真正地分配物理内存 。
|
||||
|
||||
你可能会有疑问,如果进程只是调用malloc()或者mmap()而不去写这些地址,即不去给它分配物理内存,是不是就不用担心内存泄漏了?
|
||||
|
||||
答案是这依然需要关注内存泄露,因为这可能导致进程虚拟地址空间耗尽,即虚拟地址空间同样存在内存泄露的问题。我们在下节课的案例篇中,也会分析对应的案例 ,这里先不展开描述了。
|
||||
|
||||
接下来,我们继续用一张图片来细化一下分页的过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/68/5e2dacf3890cd9d508d3e0181a8ac868.jpg" alt="">
|
||||
|
||||
如上图所示,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中字段的对应关系:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/1d/a9615117becc0244a2a23802a9cf5c1d.jpg" alt="">
|
||||
|
||||
另外如果你观察仔细的话,可能会发现,有些时候所有进程的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),每一列的含义如下。
|
||||
|
||||
- **Mapping**,用来表示文件映射中占用内存的文件,比如sshd这个可执行文件,或者堆[heap],或者栈[stack],或者其他,等等。
|
||||
- **Mode**,它是该内存的权限,比如,“r-x”是可读可执行,它往往是代码段(Text Segment);“rw-”是可读可写,这部分往往是数据段(Data Segment);“r–”是只读,这往往是数据段中的只读部分。
|
||||
- **Address、Kbytes、RSS、Dirty**,Address和Kbytes分别表示起始地址和虚拟内存的大小,RSS(Resident Set Size)则表示虚拟内存中已经分配的物理内存的大小,Dirty则表示内存中数据未同步到磁盘的字节数。
|
||||
|
||||
可以看到,通过pmap我们能够清楚地观察一个进程的整个的地址空间,包括它们分配的物理内存大小,这非常有助于我们对进程的内存使用概况做一个大致的判断。比如说,如果地址空间中[heap]太大,那有可能是堆内存产生了泄漏;再比如说,如果进程地址空间包含太多的vma(可以把maps中的每一行理解为一个vma),那很可能是应用程序调用了很多mmap而没有munmap;再比如持续观察地址空间的变化,如果发现某些项在持续增长,那很可能是那里存在问题。
|
||||
|
||||
pmap同样也是解析的/proc里的文件,具体文件是/proc/[pid]/maps和/proc/[pid]/smaps,其中smaps文件相比maps的内容更详细,可以理解为是对maps的一个扩展。你可以对比/proc/[pid]/maps和pmaps的输出,你会发现二者的内容是一致的。
|
||||
|
||||
除了观察进程自身的内存外,我们还可以观察进程分配的内存和系统指标的关联,我们就以常用的/proc/meminfo为例,来说明我们上面提到的四种内存类型(私有匿名,私有文件,共享匿名,共享文件)是如何体现在系统指标中的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/9f/9afa1845b3a308e2ae5b84a8b798f49f.jpg" alt="">
|
||||
|
||||
如上图所示,凡是私有的内存都会体现在/proc/meminfo中的AnonPages这一项,凡是共享的内存都会体现在Cached这一项,匿名共享的则还会体现在Shmem这一项。
|
||||
|
||||
```
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Cached: 3799380 kB
|
||||
...
|
||||
AnonPages: 1060684 kB
|
||||
...
|
||||
Shmem: 8724 kB
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
同样地,我也建议你动手写一些测试用例来观察下,这样你理解得就会更深刻。
|
||||
|
||||
我们对进程的内存管理相关的基础知识就先讲到这里,在下节课我们来讲一讲内存泄漏的实际案例以及其危害。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课我们讲述进程内存管理相关的一些知识,包括进程的虚拟内存与物理内存,要点如下。
|
||||
|
||||
- 进程直接读写的都是虚拟地址,虚拟地址最终会通过Paging(分页)来转换为物理内存的地址,Paging这个过程是由内核来完成的。
|
||||
- 进程的内存类型可以从anon(匿名)与file(文件)、private(私有)与shared(共享)这四项来区分为4种不同的类型,进程相关的所有内存都是这几种方式的不同组合。
|
||||
- 查看进程内存时,可以先使用top来看系统中各个进程的内存使用概况,再使用pmap去观察某个进程的内存细节。
|
||||
|
||||
进程的内存管理涉及到非常多的术语,对于常用的一些术语,比如VIRT、RES、SHR等,你还是需要牢记它们的含义的,只有熟练掌握了它们的含义,你在分析内存问题时才会更加地得心应手。比如说,如果RES太高而SHR不高,那可能是堆内存泄漏;如果SHR很高,那可能是tmpfs/shm之类的数据在持续增长,如果VIRT很高而RES很小,那可能是进程不停地在申请内存,但是却没有对这些内存进行任何的读写操作,即虚拟地址空间存在内存泄漏。
|
||||
|
||||
同样地,我希望你自己可以写一些测试用例来观察这些指标的变化。
|
||||
|
||||
## 课后作业
|
||||
|
||||
课后你可以写一些测试程序,分别分配我们这堂课提到的四种不同类型的内存,观察进程地址空间的变化,以及系统内存指标的变化。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
202
极客时间专栏/Linux内核技术实战课/内存泄漏问题/07 案例篇 | 如何预防内存泄漏导致的系统假死?.md
Normal file
202
极客时间专栏/Linux内核技术实战课/内存泄漏问题/07 案例篇 | 如何预防内存泄漏导致的系统假死?.md
Normal file
@@ -0,0 +1,202 @@
|
||||
<audio id="audio" title="07 案例篇 | 如何预防内存泄漏导致的系统假死?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/56/29f0f8f5754fe45ced47a62d7d45c656.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
上节课,我们讲了有哪些进程的内存类型会容易引起内存泄漏,这一讲我们来聊一聊,到底应该如何应对内存泄漏的问题。
|
||||
|
||||
我们知道,内存泄漏是件非常容易发生的事,但如果它不会给应用程序和系统造成危害,那它就不会构成威胁。当然我不是说这类内存泄漏无需去关心,对追求完美的程序员而言,还是需要彻底地解决掉它的。
|
||||
|
||||
而有一些内存泄漏你却需要格外重视,比如说长期运行的后台进程的内存泄漏,这种泄漏日积月累,会逐渐耗光系统内存,甚至会引起系统假死。
|
||||
|
||||
我们在了解内存泄漏造成的危害之前,先一起看下什么样的内存泄漏是有危害的。
|
||||
|
||||
## 什么样的内存泄漏是有危害的?
|
||||
|
||||
下面是一个内存泄漏的简单示例程序。
|
||||
|
||||
```
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024) /* 1G */
|
||||
int main()
|
||||
{
|
||||
char *p = malloc(SIZE);
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
memset(p, 1, SIZE);
|
||||
/* 然后就再也不使用这块内存空间 */
|
||||
/* 没有释放p所指向的内存进程就退出了 */
|
||||
/* free(p); */
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,这个程序里面申请了1G的内存后,没有进行释放就退出了,那这1G的内存空间是泄漏了吗?
|
||||
|
||||
我们可以使用一个简单的内存泄漏检查工具(valgrind)来看看。
|
||||
|
||||
```
|
||||
$ valgrind --leak-check=full ./a.out
|
||||
==20146== HEAP SUMMARY:
|
||||
==20146== in use at exit: 1,073,741,824 bytes in 1 blocks
|
||||
==20146== total heap usage: 1 allocs, 0 frees, 1,073,741,824 bytes allocated
|
||||
==20146==
|
||||
==20146== 1,073,741,824 bytes in 1 blocks are possibly lost in loss record 1 of 1
|
||||
==20146== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
|
||||
==20146== by 0x400543: main (in /home/yafang/test/mmleak/a.out)
|
||||
==20146==
|
||||
==20146== LEAK SUMMARY:
|
||||
==20146== definitely lost: 0 bytes in 0 blocks
|
||||
==20146== indirectly lost: 0 bytes in 0 blocks
|
||||
==20146== possibly lost: 1,073,741,824 bytes in 1 blocks
|
||||
==20146== still reachable: 0 bytes in 0 blocks
|
||||
==20146== suppressed: 0 bytes in 0 blocks
|
||||
|
||||
```
|
||||
|
||||
从valgrind的检查结果里我们可以清楚地看到,申请的内存只被使用了一次(memset)就再没被使用,但是在使用完后却没有把这段内存空间给释放掉,这就是典型的内存泄漏。那这个内存泄漏是有危害的吗?
|
||||
|
||||
这就要从进程地址空间的分配和销毁来说起,下面是一个简单的示意图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/64/e0e227529ba7f2fcab1ab445c4634764.jpg" alt="" title="进程地址空间申请和释放示意图">
|
||||
|
||||
从上图可以看出,进程在退出的时候,会把它建立的映射都给解除掉。换句话说,进程退出时,会把它申请的内存都给释放掉,这个内存泄漏就是没危害的。不过话说回来,虽然这样没有什么危害,但是我们最好还是要在程序里加上free §,这才是符合编程规范的。我们修改一下这个程序,加上free§,再次编译后通过valgrind来检查,就会发现不存在任何内存泄漏了:
|
||||
|
||||
```
|
||||
$ valgrind --leak-check=full ./a.out
|
||||
==20123== HEAP SUMMARY:
|
||||
==20123== in use at exit: 0 bytes in 0 blocks
|
||||
==20123== total heap usage: 1 allocs, 1 frees, 1,073,741,824 bytes allocated
|
||||
==20123==
|
||||
==20123== All heap blocks were freed -- no leaks are possible
|
||||
|
||||
```
|
||||
|
||||
总之,如果进程不是长时间运行,那么即使存在内存泄漏(比如这个例子中的只有malloc没有free),它的危害也不大,因为进程退出时,内核会把进程申请的内存都给释放掉。
|
||||
|
||||
我们前面举的这个例子是对应用程序无害的内存泄漏,我们继续来看下哪些内存泄漏会给应用程序产生危害 。我们同样以malloc为例,看一个简单的示例程序:
|
||||
|
||||
```
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024) /* 1G */
|
||||
|
||||
void process_memory()
|
||||
{
|
||||
char *p;
|
||||
p = malloc(SIZE);
|
||||
if (!p)
|
||||
return;
|
||||
memset(p, 1, SIZE);
|
||||
/* Forget to free this memory */
|
||||
}
|
||||
|
||||
/* 处理其他事务,为了简便起见,我们就以sleep为例 */
|
||||
void process_others()
|
||||
{
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
/* 这部分内存只处理一次,以后再也不会用到 */
|
||||
process_memory();
|
||||
|
||||
|
||||
/* 进程会长时间运行 */
|
||||
while (1) {
|
||||
process_others();
|
||||
}
|
||||
return 0;
|
||||
|
||||
```
|
||||
|
||||
这是一个长时间运行的程序,process_memory()中我们申请了1G的内存去使用,然后就再也不用它了,由于这部分内存不会再被利用,这就造成了内存的浪费,如果这样的程序多了,被泄漏出去的内存就会越来越多,然后系统中的可用内存就会越来越少。
|
||||
|
||||
对于后台服务型的业务而言,基本上都是需要长时间运行的程序,所以后台服务的内存泄漏会给系统造成实际的危害。那么,究竟会带来什么样的危害,我们又该如何去应对呢?
|
||||
|
||||
## 如何预防内存泄漏导致的危害?
|
||||
|
||||
我们还是以上面这个malloc()程序为例,在这个例子中,它只是申请了1G的内存,如果说持续不断地申请内存而不释放,你会发现,很快系统内存就会被耗尽,进而触发OOM killer去杀进程。这个信息可以通过dmesg(该命令是用来查看内核日志的)这个命令来查看:
|
||||
|
||||
```
|
||||
$ dmesg
|
||||
[944835.029319] a.out invoked oom-killer: gfp_mask=0x100dca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO), order=0, oom_score_adj=0
|
||||
[...]
|
||||
[944835.052448] Out of memory: Killed process 1426 (a.out) total-vm:8392864kB, anon-rss:7551936kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:14832kB oom_score_adj:0
|
||||
|
||||
```
|
||||
|
||||
系统内存不足时会唤醒OOM killer来选择一个进程给杀掉,在我们这个例子中它杀掉了这个正在内存泄漏的程序,该进程被杀掉后,整个系统也就变得安全了。但是你要注意,**OOM killer选择进程是有策略的,它未必一定会杀掉正在内存泄漏的进程,很有可能是一个无辜的进程被杀掉。**而且,OOM本身也会带来一些副作用。
|
||||
|
||||
我来说一个发生在生产环境中的实际案例,这个案例我也曾经[反馈给Linux内核社区来做改进](https://lore.kernel.org/linux-mm/1586597774-6831-1-git-send-email-laoar.shao@gmail.com/),接下来我们详细说一下它。
|
||||
|
||||
这个案例跟OOM日志有关,OOM日志可以理解为是一个单生产者多消费者的模型,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/a7/b39503a3fb39e731d2d4c51687db70a7.jpg" alt="" title="OOM info">
|
||||
|
||||
这个单生产者多消费者模型,其实是由OOM killer打印日志(OOM info)时所使用的printk(类似于userspace的printf)机制来决定的。printk会检查这些日志需要输出给哪些消费者,比如写入到内核缓冲区(kernel buffer),然后通过dmesg命令来查看;我们通常也都会配置rsyslog,然后rsyslogd会将内核缓冲区的内容给转储到日志文件(/var/log/messages)中;服务器也可能会连着一些控制台(console ),比如串口,这些日志也会输出到这些console。
|
||||
|
||||
问题就出在console这里,如果console的速率很慢,输出太多日志会非常消耗时间,而当时我们配置了“console=ttyS1,19200”,即波特率为19200的串口,这是个很低速率的串口。一个完整的OOM info需要约10s才能打印完,这在系统内存紧张时就会成为一个瓶颈点,为什么会是瓶颈点呢?答案如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2c/e7/2c4e5452584e9a1525921dffbdfda4e7.jpg" alt="" title="OOM为什么会成为瓶颈点">
|
||||
|
||||
进程A在申请内存失败后会触发OOM,在发生OOM的时候会打印很多很多日志(这些日志是为了方便分析为什么OOM会发生),然后会选择一个合适的进程来杀掉,从而释放出来空闲的内存,这些空闲的内存就可以满足后续内存申请了。
|
||||
|
||||
如果这个OOM的过程耗时很长(即打印到slow console所需的时间太长,如上图红色部分所示),其他进程(进程B)也在此时申请内存,也会申请失败,于是进程B同样也会触发OOM来尝试释放内存,而OOM这里又有一个全局锁(oom_lock)来进行保护,进程B尝试获取(trylock)这个锁的时候会失败,就只能再次重试。
|
||||
|
||||
如果此时系统中有很多进程都在申请内存,那么这些申请内存的进程都会被阻塞在这里,这就形成了一个恶性循环,甚至会引发系统长时间无响应(假死)。
|
||||
|
||||
针对这个问题,我与Linux内核内存子系统的维护者Michal Hocko以及OOM子模块的活跃开发者Tetsuo Handa进行了[一些讨论](https://lore.kernel.org/linux-mm/1586597774-6831-1-git-send-email-laoar.shao@gmail.com/),不过我们并没有讨论出一个完美的解决方案,目前仍然是只有一些规避措施,如下:
|
||||
|
||||
<li>
|
||||
<p>**在发生OOM时尽可能少地打印信息**<br>
|
||||
通过将[vm.oom_dump_tasks](https://www.kernel.org/doc/Documentation/sysctl/vm.txt)调整为0,可以不去备份(dump)当前系统中所有可被kill的进程信息,如果系统中有很多进程,这些信息的打印可能会非常消耗时间。在我们这个案例里,这部分耗时约为6s多,占OOM整体耗时10s的一多半,所以减少这部分的打印能够缓解这个问题。</p>
|
||||
但是,**这并不是一个完美的方案,只是一个规避措施**。因为当我们把vm.oom_dump_tasks配置为1时,是可以通过这些打印的信息来检查OOM killer是否选择了合理的进程,以及系统中是否存在不合理的OOM配置策略的。如果我们将它配置为0,就无法得到这些信息了,而且这些信息不仅不会打印到串口,也不会打印到内核缓冲区,导致无法被转储到不会产生问题的日志文件中。
|
||||
</li>
|
||||
<li>
|
||||
<p>**调整串口打印级别,不将OOM信息打印到串口**<br>
|
||||
通过调整[/proc/sys/kernel/printk](https://www.kernel.org/doc/Documentation/sysctl/kernel.txt)可以做到避免将OOM信息输出到串口,我们通过设置console_loglevel来将它的级别设置的比OOM日志级别(为4)小,就可以避免OOM的信息打印到console,比如将它设置为3:</p>
|
||||
</li>
|
||||
|
||||
```
|
||||
# 初始配置(为7):所有信息都会输出到console
|
||||
$ cat /proc/sys/kernel/printk
|
||||
7 4 1 7
|
||||
|
||||
# 调整console_loglevel级别,不让OOM信息打印到console
|
||||
$ echo "3 4 1 7" > /proc/sys/kernel/printk
|
||||
|
||||
# 查看调整后的配置
|
||||
$ cat /proc/sys/kernel/printk
|
||||
3 4 1
|
||||
|
||||
```
|
||||
|
||||
但是这样做会导致所有低于默认级别(为4)的内核日志都无法输出到console,在系统出现问题时,我们有时候(比如无法登录到服务器上面时)会需要查看console信息来判断问题是什么引起的,如果某些信息没有被打印到console,可能会影响我们的分析。
|
||||
|
||||
这两种规避方案各有利弊,你需要根据你的实际情况来做选择,如果你不清楚怎么选择时,我建议你选择第二种,因为我们使用console的概率还是较少一些,所以第二种方案的影响也相对较小一些。
|
||||
|
||||
OOM相关的一些日志输出后,就到了下一个阶段:选择一个最需要杀死的进程来杀掉。OOM killer在选择杀掉哪个进程时,也是一个比较复杂的过程,而且如果配置不当也会引起其他问题。关于这部分的案例,我们会在下节课来分析。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课我们讲了什么是内存泄漏,以及内存泄漏可能造成的危害。对于长时间运行的后台任务而言,它存在的内存泄漏可能会给系统带来比较严重的危害,所以我们一定要重视这些任务的内存泄漏问题。
|
||||
|
||||
内存泄漏问题是非常容易发生的,所以我们需要提前做好内存泄漏的兜底工作:即使有泄漏了也不要让它给系统带来很大的危害。长时间的内存泄漏问题最后基本都会以OOM结束,所以你需要去掌握OOM的相关知识,来做好这个兜底工作。
|
||||
|
||||
如果你的服务器有慢速的串口设备,那你一定要防止它接收太多的日志,尤其是OOM产生的日志,因为OOM的日志量是很大的,打印完整个OOM信息kennel会很耗时,进而导致阻塞申请内存的进程,甚至会严重到让整个系统假死。
|
||||
|
||||
墨菲定律告诉我们,如果事情有变坏的可能,不管这种可能性有多小,它总会发生。对应到内存泄漏就是,当你的系统足够复杂后,它总是可能会发生的。所以,对于内存泄漏问题,你在做好预防的同时,也一定要对它发生后可能带来的危害做好预防。
|
||||
|
||||
## 课后作业
|
||||
|
||||
请写一些应用程序来构造内存泄漏的测试用例,然后使用valgrind来进行观察。欢迎在留言区分享你的看法。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
130
极客时间专栏/Linux内核技术实战课/内存泄漏问题/08 案例篇 | Shmem:进程没有消耗内存,内存哪去了?.md
Normal file
130
极客时间专栏/Linux内核技术实战课/内存泄漏问题/08 案例篇 | Shmem:进程没有消耗内存,内存哪去了?.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<audio id="audio" title="08 案例篇 | Shmem:进程没有消耗内存,内存哪去了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/49/07/4933d2de6c743f80ff4fd01bbd29d507.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
在前一节课,我们讲述了进程堆内存的泄漏以及因为内存泄漏而导致的OOM的危害。这节课我们继续讲其他类型的内存泄漏,这样你在发现系统内存越来越少时,就能够想到会是什么在消耗内存。
|
||||
|
||||
有的内存泄漏会体现在进程内存里面,这种相对好观察些;而有的内存泄漏就很难观察了,因为它们无法通过观察进程消耗的内存来进行判断,从而容易被忽视,比如Shmem内存泄漏就属于这种容易被忽视的,这节课我们重点来讲讲它。
|
||||
|
||||
## 进程没有消耗内存,内存哪去了?
|
||||
|
||||
我生产环境上就遇到过一个真实的案例。我们的运维人员发现某几台机器used(已使用的)内存越来越多,但是通过top以及其他一些命令,却检查不出来到底是谁在占用内存。随着可用内存变得越来越少,业务进程也被OOM killer给杀掉,这给业务带来了比较严重的影响。于是他们向我寻求帮助,看看产生问题的原因是什么。
|
||||
|
||||
我在之前的课程中也提到过,在遇到系统内存不足时,我们首先要做的是查看/proc/meminfo中哪些内存类型消耗较多,然后再去做针对性分析。但是如果你不清楚/proc/meminfo里面每一项的含义,即使知道了哪几项内存出现了异常,也不清楚该如何继续去分析。所以你最好是记住/proc/meminfo里每一项的含义。
|
||||
|
||||
回到我们这个案例,通过查看这几台服务器的/proc/meminfo,发现是Shmem的大小有些异常:
|
||||
|
||||
```
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Shmem 16777216 kB
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
那么Shmem这一项究竟是什么含义呢?该如何去进一步分析到底是谁在使用Shmem呢?
|
||||
|
||||
我们在前面的基础篇里提到,Shmem是指匿名共享内存,即进程以mmap(MAP_ANON|MAP_SHARED)这种方式来申请的内存。你可能会有疑问,进程以这种方式来申请的内存不应该是属于进程的RES(resident)吗?比如下面这个简单的示例:
|
||||
|
||||
```
|
||||
#include <sys/mman.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#define SIZE (1024*1024*1024)
|
||||
|
||||
int main()
|
||||
{
|
||||
char *p;
|
||||
|
||||
p = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
|
||||
if (!p)
|
||||
return -1;
|
||||
|
||||
memset(p, 1, SIZE);
|
||||
|
||||
while (1) {
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
运行该程序后,通过top可以看到确实会体现在进程的RES里面,而且还同时体现在了进程的SHR里面,也就是说,如果进程是以mmap这种方式来申请内存的话,我们是可以通过进程的内存消耗来观察到的。
|
||||
|
||||
但是在我们生产环境上遇到的问题,各个进程的RES都不大,看起来和/proc/meminfo中的Shmem完全对应不起来,这又是为什么呢?
|
||||
|
||||
先说答案:这跟一种特殊的Shmem有关。我们知道,磁盘的速度是远远低于内存的,有些应用程序为了提升性能,会避免将一些无需持续化存储的数据写入到磁盘,而是把这部分临时数据写入到内存中,然后定期或者在不需要这部分数据时,清理掉这部分内容来释放出内存。在这种需求下,就产生了一种特殊的Shmem:tmpfs。tmpfs如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/24/eb/248b083e0c263b096c66f8078e3a3aeb.jpg" alt="">
|
||||
|
||||
它是一种内存文件系统,只存在于内存中,它无需应用程序去申请和释放内存,而是操作系统自动来规划好一部分空间,应用程序只需要往这里面写入数据就可以了,这样会很方便。我们可以使用moun命令或者df命令来看系统中tmpfs的挂载点:
|
||||
|
||||
```
|
||||
$ df -h
|
||||
Filesystem Size Used Avail Use% Mounted on
|
||||
...
|
||||
tmpfs 16G 15G 1G 94% /run
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
就像进程往磁盘写文件一样,进程写完文件之后就把文件给关闭掉了,这些文件和进程也就不再有关联,所以这些磁盘文件的大小不会体现在进程中。同样地,tmpfs中的文件也一样,它也不会体现在进程的内存占用上。讲到这里,你大概已经猜到了,我们Shmem占用内存多,是不是因为Shmem中的tmpfs较大导致的呢?
|
||||
|
||||
tmpfs是属于文件系统的一种。对于文件系统,我们都可以通过df来查看它的使用情况。所以呢,我们也可以通过df来看是不是tmpfs占用的内存较多,结果发现确实是它消耗了很多内存。这个问题就变得很清晰了,我们只要去分析tmpfs中存储的是什么文件就可以了。
|
||||
|
||||
我们在生产环境上还遇到过这样一个问题:systemd不停地往tmpfs中写入日志但是没有去及时清理,而tmpfs配置的初始值又太大,这就导致systemd产生的日志量越来越多,最终可用内存越来越少。
|
||||
|
||||
针对这个问题,解决方案就是限制systemd所使用的tmpfs的大小,在日志量达到tmpfs大小限制时,自动地清理掉临时日志,或者定期清理掉这部分日志,这都可以通过systemd的配置文件来做到。tmpfs的大小可以通过如下命令(比如调整为2G)调整:
|
||||
|
||||
```
|
||||
$ mount -o remount,size=2G /run
|
||||
|
||||
```
|
||||
|
||||
tmpfs作为一种特殊的Shmem,它消耗的内存是不会体现在进程内存中的,这往往会给问题排查带来一些难度。要想高效地分析这种类型的问题,你必须要去熟悉系统中的内存类型。除了tmpfs之外,其他一些类型的内存也不会体现在进程内存中,比如内核消耗的内存:/proc/meminfo中的Slab(高速缓存)、KernelStack(内核栈)和VmallocUsed(内核通过vmalloc申请的内存),这些也是你在不清楚内存被谁占用时需要去排查的。
|
||||
|
||||
如果tmpfs消耗的内存越积越多而得不到清理,最终的结果也是系统可用内存不足,然后触发OOM来杀掉进程。它很有可能会杀掉很重要的进程,或者是那些你认为不应该被杀掉的进程。
|
||||
|
||||
## OOM杀进程的危害
|
||||
|
||||
OOM杀进程的逻辑大致如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/ee/150863953f090f09179e87814322a5ee.jpg" alt="">
|
||||
|
||||
OOM killer在杀进程的时候,会把系统中可以被杀掉的进程扫描一遍,根据进程占用的内存以及配置的oom_score_adj来计算出进程最终的得分,然后把得分(oom_score)最大的进程给杀掉,如果得分最大的进程有多个,那就把先扫描到的那个给杀掉。
|
||||
|
||||
进程的oom_score可以通过/proc/[pid]/oom_score来查看,你可以扫描一下你系统中所有进程的oom_score,其中分值最大的那个就是在发生OOM时最先被杀掉的进程。不过你需要注意,由于oom_score和进程的内存开销有关,而进程的内存开销又是会动态变化的,所以该值也会动态变化。
|
||||
|
||||
如果你不想这个进程被首先杀掉,那你可以调整该进程的oom_score_adj改变这个oom_score;如果你的进程无论如何都不能被杀掉,那你可以将oom_score_adj配置为-1000。
|
||||
|
||||
通常而言,我们都需要将一些很重要的系统服务的oom_score_adj配置为-1000,比如sshd,因为这些系统服务一旦被杀掉,我们就很难再登陆进系统了。
|
||||
|
||||
但是,除了系统服务之外,不论你的业务程序有多重要,都尽量不要将它配置为-1000。因为你的业务程序一旦发生了内存泄漏,而它又不能被杀掉,这就会导致随着它的内存开销变大,OOM killer不停地被唤醒,从而把其他进程一个个给杀掉,我们之前在生产环境中就遇到过类似的案例。
|
||||
|
||||
OOM killer的作用之一,就是找到系统中不停泄漏内存的进程然后把它给杀掉,如果没有找对,那就会误杀其他进程,甚至是误杀了更为重要的业务进程。
|
||||
|
||||
OOM killer除了会杀掉一些无辜进程外,它选择杀进程的策略也未必是正确的。接下来又到了给内核找茬的时刻了,这也是我们这个系列课程的目的:告诉你如何来学些Linux内核,但同时我也要告诉你,要对内核有怀疑态度。下面这个案例就是一个内核的Bug。
|
||||
|
||||
在我们的一个服务器上,我们发现OOM killer在杀进程的时候,总是会杀掉最先扫描到的进程,而由于先扫描到的进程的内存太小,就导致OOM杀掉进程后很难释放出足够多的内存,然后很快再次发生OOM。
|
||||
|
||||
这是在Kubernetes环境下触发的一个问题,Kubernetes会将某些重要的容器配置为Guaranteed ([对应的oom_score_adj为-998](https://kubernetes.io/docs/tasks/administer-cluster/out-of-resource/)),以防止系统OOM的时候把该重要的容器给杀掉。 然而,如果容器内部发生了OOM就会触发这个内核Bug,导致总是杀掉最先扫描到的那个进程。
|
||||
|
||||
针对该内核Bug,我也给社区贡献了一个patch([mm, oom: make the calculation of oom badness more accurate](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9066e5cfb73cdbcdbb49e87999482ab615e9fc76))来修复这个选择不到合适进程的问题,在这个patch的commit log里我详细地描述了该问题,感兴趣的话你可以去看下。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课,我们学习了tmpfs这种类型的内存泄漏以及它的观察方法,这种类型的内存泄漏和其他进程内存泄漏最大的不同是,你很难通过进程消耗的内存来判断是哪里在泄漏,因为这种类型的内存不会体现在进程的RES中。但是,如果你熟悉内存问题的常规分析方法,你就能很快地找到问题所在。
|
||||
|
||||
- 在不清楚内存被谁消耗时,你可以通过/proc/meminfo找到哪种类型的内存开销比较大,然后再对这种类型的内存做针对性分析。
|
||||
- 你需要配置合适的OOM策略(oom_score_adj)来防止重要的业务被过早杀掉(比如将重要业务的oom_score_adj调小为负值),同时你也需要考虑误杀其他进程,你可以通过比较进程的/proc/[pid]/oom_score,来判断出进程被杀的先后顺序。
|
||||
- 再次强调一遍,你需要学习内核,但同时你也需要对内核持怀疑态度。
|
||||
|
||||
总之,你对不同内存类型的特点了解越多,你在分析内存问题的时候(比如内存泄漏问题)就会更加高效。熟练掌握这些不同的内存类型,你也能够在业务需要申请内存时选择合适的内存类型。
|
||||
|
||||
## 课后作业
|
||||
|
||||
请你运行几个程序,分别设置不同的oom_score_adj,并记录下它们的oom_score是什么样的,然后消耗系统内存触发OOM,看看oom_score和进程被杀的顺序是什么关系。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
251
极客时间专栏/Linux内核技术实战课/内存泄漏问题/09 分析篇 | 如何对内核内存泄漏做些基础的分析?.md
Normal file
251
极客时间专栏/Linux内核技术实战课/内存泄漏问题/09 分析篇 | 如何对内核内存泄漏做些基础的分析?.md
Normal file
@@ -0,0 +1,251 @@
|
||||
<audio id="audio" title="09 分析篇 | 如何对内核内存泄漏做些基础的分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/01/25/01426282302497e9cd96a67f0ae86525.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
如果你是一名应用开发者,那你对应用程序引起的内存泄漏应该不会陌生。但是,你有没有想过,内存泄漏也可能是由操作系统(内核)自身的问题引起的呢?这是很多应用开发者以及运维人员容易忽视的地方,或者是相对陌生的领域。
|
||||
|
||||
然而陌生的领域不代表不会有问题,如果在陌生的领域发生了问题,而你总是习惯于分析应用程序自身,那你可能要浪费很多的分析时间,却依然一无所获。所以,对于应用开发者或者运维人员而言,掌握基本的内核内存泄漏分析方法也是必需的,这样在它发生问题时,你可以有一个初步的判断,而不至于一筹莫展。
|
||||
|
||||
内核内存泄漏往往都会是很严重的问题,这通常意味着要重启服务器来解决了,我们肯定并不希望只能靠重启服务器来解决它,不然那就只能没完没了地重启了。我们希望的应该是,在发生了内存泄漏后,能够判断出来是不是内核引起的问题,以及能够找到引起问题的根因,或者是向更专业的内核开发者求助来找到问题根因,从而彻底解决掉它,以免再次重启服务器。
|
||||
|
||||
那么,我们该如何判断内存泄漏是否是内核导致的呢?这节课我们就来讲一讲内核内存泄漏的基础分析方法。
|
||||
|
||||
## 内核内存泄漏是什么?
|
||||
|
||||
在进行具体的分析之前,我们需要先对内核内存泄漏有个初步的概念,究竟内核内存泄漏是指什么呢?这得从内核空间内存分配的基本方法说起。
|
||||
|
||||
我们在[06基础篇](https://time.geekbang.org/column/article/280455)里讲过,进程的虚拟地址空间(address space)既包括用户地址空间,也包括内核地址空间。这可以简单地理解为,进程运行在用户态申请的内存,对应的是用户地址空间,进程运行在内核态申请的内存,对应的是内核地址空间,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/d9/41909181c0f6aa0958c33df52cd626d9.jpg" alt="">
|
||||
|
||||
应用程序可以通过malloc()和free()在用户态申请和释放内存,与之对应,可以通过kmalloc()/kfree()以及vmalloc()/vfree()在内核态申请和释放内存。当然,还有其他申请和释放内存的方法,但大致可以分为这两类。
|
||||
|
||||
从最右侧的物理内存中你可以看出这两类内存申请方式的主要区别,kmalloc()内存的物理地址是连续的,而vmalloc()内存的物理地址则是不连续的。这两种不同类型的内存也是可以通过/proc/meminfo来观察的:
|
||||
|
||||
```
|
||||
$ cat /proc/meminfo
|
||||
...
|
||||
Slab: 2400284 kB
|
||||
SReclaimable: 47248 kB
|
||||
SUnreclaim: 2353036 kB
|
||||
...
|
||||
VmallocTotal: 34359738367 kB
|
||||
VmallocUsed: 1065948 kB
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
其中vmalloc申请的内存会体现在VmallocUsed这一项中,即已使用的Vmalloc区大小;而kmalloc申请的内存则是体现在Slab这一项中,它又分为两部分,其中SReclaimable是指在内存紧张的时候可以被回收的内存,而SUnreclaim则是不可以被回收只能主动释放的内存。
|
||||
|
||||
内核之所以将kmalloc和vmalloc的信息通过/proc/meminfo给导出来,也是为了在它们引起问题的时候,让我们可以有方法来进行排查。在讲述具体的案例以及排查方法之前,我们先以一个简单的程序来看下内核空间是如何进行内存申请和释放的。
|
||||
|
||||
```
|
||||
/* kmem_test */
|
||||
#include <linux/init.h>
|
||||
#include <linux/vmalloc.h>
|
||||
|
||||
#define SIZE (1024 * 1024 * 1024)
|
||||
|
||||
char *kaddr;
|
||||
|
||||
char *kmem_alloc(unsigned long size)
|
||||
{
|
||||
char *p;
|
||||
p = vmalloc(size);
|
||||
if (!p)
|
||||
pr_info("[kmem_test]: vmalloc failed\n");
|
||||
return p;
|
||||
}
|
||||
|
||||
void kmem_free(const void *addr)
|
||||
{
|
||||
if (addr)
|
||||
vfree(addr);
|
||||
}
|
||||
|
||||
|
||||
int __init kmem_init(void)
|
||||
{
|
||||
pr_info("[kmem_test]: kernel memory init\n");
|
||||
kaddr = kmem_alloc(SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void __exit kmem_exit(void)
|
||||
{
|
||||
kmem_free(kaddr);
|
||||
pr_info("[kmem_test]: kernel memory exit\n");
|
||||
}
|
||||
|
||||
module_init(kmem_init)
|
||||
module_exit(kmem_exit)
|
||||
|
||||
MODULE_LICENSE("GPLv2");
|
||||
|
||||
```
|
||||
|
||||
这是一个典型的内核模块,在这个内核模块中,我们使用vmalloc来分配了1G的内存空间,然后在模块退出的时候使用vfree释放掉它。这在形式上跟应用申请/释放内存其实是一致的,只是申请和释放内存的接口函数不一样而已。
|
||||
|
||||
我们需要使用Makefile来编译这个内核模块:
|
||||
|
||||
```
|
||||
obj-m = kmem_test.o
|
||||
|
||||
all:
|
||||
make -C /lib/modules/`uname -r`/build M=`pwd`
|
||||
clean:
|
||||
rm -f *.o *.ko *.mod.c *.mod *.a modules.order Module.symvers
|
||||
|
||||
```
|
||||
|
||||
执行make命令后就会生成一个kmem_test的内核模块,接着执行下面的命令就可以安装该模块了:
|
||||
|
||||
```
|
||||
$ insmod kmem_test
|
||||
|
||||
```
|
||||
|
||||
用rmmod命令则可以把它卸载掉:
|
||||
|
||||
```
|
||||
$ rmmod kmem_test
|
||||
|
||||
```
|
||||
|
||||
这个示例程序就是内核空间内存分配的基本方法。你可以在插入/卸载模块前后观察VmallocUsed的变化,以便于你更好地理解这一项的含义。
|
||||
|
||||
那么,在什么情况下会发生内核空间的内存泄漏呢?
|
||||
|
||||
跟用户空间的内存泄漏类似,内核空间的内存泄漏也是指只申请内存而不去释放该内存的情况,比如说,如果我们不在kmem_exit()这个函数中调用kmem_free(),就会产生内存泄漏问题。
|
||||
|
||||
那么,内核空间的内存泄漏与用户空间的内存泄漏有什么不同呢?我们知道,用户空间内存的生命周期与用户进程是一致的,进程退出后这部分内存就会自动释放掉。但是,内核空间内存的生命周期是与内核一致的,却不是跟内核模块一致的,也就是说,在内核模块退出时,不会自动释放掉该内核模块申请的内存,只有在内核重启(即服务器重启)时才会释放掉这部分内存。
|
||||
|
||||
总之,一旦发生内核内存泄漏,你很难有很好的方法来优雅地解决掉它,很多时候唯一的解决方案就是重启服务器,这显然是件很严重的问题。同样地,我也建议你来观察下这个行为,但是你需要做好重启服务器的心理准备。
|
||||
|
||||
kmalloc的用法跟vmalloc略有不同,你可以参考[kmalloc API](https://www.kernel.org/doc/htmldocs/kernel-api/API-kmalloc.html)和[kfree API](https://www.kernel.org/doc/htmldocs/kernel-api/API-kfree.html)来修改一下上面的测试程序,然后观察下kmalloc内存和/proc/meminfo中那几项的关系,我在这里就不做演示了,留给你作为课后作业。
|
||||
|
||||
内核内存泄漏的问题往往会发生在一些驱动程序中,比如说网卡驱动,SSD卡驱动等,以及我们自己开发的一些驱动,因为这类驱动不像Linux内核那样经历过大规模的功能验证和测试,所以相对容易出现一些隐藏很深的问题。
|
||||
|
||||
我们在生产环境上就遇到过很多起这类第三方驱动引发的内存泄漏问题,排查起来往往也比较费时。作为一个解决过很多这类问题的过来人,我对你的建议是,当你发现内核内存泄漏时,首先需要去质疑的就是你们系统中的第三方驱动程序,以及你们自己开发的驱动程序。
|
||||
|
||||
那么,我们该如何来观察内核内存泄漏呢?
|
||||
|
||||
## 如何观察内核内存泄漏?
|
||||
|
||||
在前面已经讲过,我们可以通过/proc/meminfo来观察内核内存的分配情况,这提供了一个观察内核内存的简便方法:
|
||||
|
||||
- 如果/proc/meminfo中内核内存(比如VmallocUsed和SUnreclaim)太大,那很有可能发生了内核内存泄漏;
|
||||
- 另外,你也可以周期性地观察VmallocUsed和SUnreclaim的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏。
|
||||
|
||||
/proc/meminfo只是提供了系统内存的整体使用情况,如果我们想要看具体是什么模块在使用内存,那该怎么办呢?
|
||||
|
||||
这也可以通过/proc来查看,所以再次强调一遍,当你不清楚该如何去分析时,你可以试着去查看/proc目录下的文件。以上面的程序为例,安装kmem_test这个内核模块后,我们可以通过/proc/vmallocinfo来看到该模块的内存使用情况:
|
||||
|
||||
```
|
||||
$ cat /proc/vmallocinfo | grep kmem_test
|
||||
0xffffc9008a003000-0xffffc900ca004000 1073745920 kmem_alloc+0x13/0x30 [kmem_test] pages=262144 vmalloc vpages N0=262144
|
||||
|
||||
```
|
||||
|
||||
可以看到,在[kmem_test]这个模块里,通过kmem_alloc这个函数申请了262144个pages,即总共1G大小的内存。假设我们怀疑kmem_test这个模块存在问题,我们就可以去看看kmem_alloc这个函数里申请的内存有没有释放的地方。
|
||||
|
||||
上面这个测试程序相对比较简单一些,所以根据/proc/vmallocinfo里面的信息就能够简单地看出来是否有问题。但是,生产环境中运行的一些驱动或者内核模块,在逻辑上会复杂得多,很难一眼就看出来是否存在内存泄漏,这往往需要大量的分析。
|
||||
|
||||
那对于这种复杂场景下的内核内存泄漏问题,基本的分析思路是什么样的呢?
|
||||
|
||||
## 复杂场景下内核内存泄漏问题分析思路
|
||||
|
||||
如果我们想要对内核内存泄漏做些基础的分析,最好借助一些内核内存泄漏分析工具,其中最常用的分析工具就是[kmemleak](https://www.kernel.org/doc/html/v4.10/dev-tools/kmemleak.html)。
|
||||
|
||||
kmemleak是内核内存泄漏检查的利器,但是,它的使用也存在一些不便性,因为打开该特性会给性能带来一些损耗,所以生产环境中的内核都会默认关闭该特性。该特性我们一般只用在测试环境中,然后在测试环境中运行需要分析的驱动程序以及其他内核模块。
|
||||
|
||||
与其他内存泄漏检查工具类似,kmemleak也是通过检查内核内存的申请和释放,来判断是否存在申请的内存不再使用也不释放的情况。如果存在,就认为是内核内存泄漏,然后把这些泄漏的信息通过/sys/kernel/debug/kmemleak这个文件导出给用户分析。同样以我们上面的程序为例,检查结果如下:
|
||||
|
||||
```
|
||||
unreferenced object 0xffffc9008a003000 (size 1073741824):
|
||||
comm "insmod", pid 11247, jiffies 4344145825 (age 3719.606s)
|
||||
hex dump (first 32 bytes):
|
||||
38 40 18 ba 80 88 ff ff 00 00 00 00 00 00 00 00 8@..............
|
||||
f0 13 c9 73 80 88 ff ff 18 40 18 ba 80 88 ff ff ...s.....@......
|
||||
backtrace:
|
||||
[<00000000fbd7cb65>] __vmalloc_node_range+0x22f/0x2a0
|
||||
[<000000008c0afaef>] vmalloc+0x45/0x50
|
||||
[<000000004f3750a2>] 0xffffffffa0937013
|
||||
[<0000000078198a11>] 0xffffffffa093c01a
|
||||
[<000000002041c0ec>] do_one_initcall+0x4a/0x200
|
||||
[<000000008d10d1ed>] do_init_module+0x60/0x220
|
||||
[<000000003c285703>] load_module+0x156c/0x17f0
|
||||
[<00000000c428a5fe>] __do_sys_finit_module+0xbd/0x120
|
||||
[<00000000bc613a5a>] __x64_sys_finit_module+0x1a/0x20
|
||||
[<000000004b0870a2>] do_syscall_64+0x52/0x90
|
||||
[<000000002f458917>] entry_SYSCALL_64_after_hwframe+0x44/0xa9
|
||||
|
||||
```
|
||||
|
||||
由于该程序通过vmalloc申请的内存以后再也没有使用,所以被kmemleak标记为了“unreferenced object”,我们需要在使用完该内存空间后就释放它以节省内存。
|
||||
|
||||
如果我们想在生产环境上来观察内核内存泄漏,就无法使用kmemleak了,那还有没有其他的方法呢?
|
||||
|
||||
我们可以使用内核提供的内核内存申请释放的tracepoint,来动态观察内核内存使用情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/8c/4c434f56b5c41f9cc2eb53a2c98f948c.jpg" alt="">
|
||||
|
||||
当我们使能这些tracepoints后,就可以观察内存的动态申请和释放情况了,只是这个分析过程不如kmemleak那么高效。
|
||||
|
||||
当我们想要观察某些内核结构体的申请和释放时,可能没有对应的tracepiont。这个时候就需要使用kprobe或者systemtap,来针对具体的内核结构体申请释放函数进行追踪了。下面就是我们在生产环境中的一个具体案例。
|
||||
|
||||
业务方反馈说docker里面的可用内存越来越少,不清楚是什么状况,在我们通过/proc下面的文件(/proc/slabinfo)判断出来是dentry消耗内存过多后,写了一个systemtap脚本来观察dentry的申请和释放:
|
||||
|
||||
```
|
||||
# dalloc_dfree.stp
|
||||
# usage : stap -x pid dalloc_dfree.stp
|
||||
global free = 0;
|
||||
global alloc = 0;
|
||||
|
||||
probe kernel.function("d_free") {
|
||||
if (target() == pid()) {
|
||||
free++;
|
||||
}
|
||||
}
|
||||
|
||||
probe kernel.function("d_alloc").return {
|
||||
if (target() == pid()) {
|
||||
alloc++;
|
||||
}
|
||||
}
|
||||
|
||||
probe end {
|
||||
printf("alloc %d free %d\n", alloc, free);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们使用该工具进行了多次统计,都发现是dentry的申请远大于它的释放:
|
||||
|
||||
```
|
||||
alloc 2041 free 1882
|
||||
alloc 18137 free 6852
|
||||
alloc 22505 free 10834
|
||||
alloc 33118 free 20531
|
||||
|
||||
```
|
||||
|
||||
于是,我们判断在容器环境中dentry的回收存在问题,最终定位出这是3.10版本内核的一个Bug: 如果docker内部内存使用达到了limit,但是全局可用内存还很多,那就无法去回收docker内部的slab了。当然,这个Bug在[新版本内核上已经fix了](https://lwn.net/Articles/628829/)。
|
||||
|
||||
好了,我们这节课就讲到这里。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课我们讲了一种更难分析以及引起危害更大的内存泄漏:内核内存泄漏。我们还讲了针对这种内存泄漏的常用分析方法:
|
||||
|
||||
- 你可以通过/proc/meminfo里面的信息来看内核内存的使用情况,然后根据这里面的信息来做一些基本的判断:如果内核太大那就值得怀疑;
|
||||
- kmemleak是内核内存分析的利器,但是一般只在测试环境上使用它,因为它对性能会有比较明显的影响;
|
||||
- 在生产环境中可以使用tracepoint或者kprobe,来追踪特定类型内核内存的申请和释放,从而帮助我们判断是否存在内存泄漏。但这往往需要专业的知识,你在不明白的时候可以去请教一些内核专家;
|
||||
- 内核内存泄漏通常都是第三方驱动或者自己写的一些内核模块导致的,在出现内核内存泄漏时,你可以优先去排查它们。
|
||||
|
||||
## 课后作业
|
||||
|
||||
我们这节课讲的内容对应用开发者会有些难度,对于运维人员而言也是需要掌握的。所以我们的课后作业主要是针对运维人员或者内核初学者的:请写一个systemtap脚本来追踪内核内存的申请和释放。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
245
极客时间专栏/Linux内核技术实战课/内存泄漏问题/10 分析篇 | 内存泄漏时,我们该如何一步步找到根因?.md
Normal file
245
极客时间专栏/Linux内核技术实战课/内存泄漏问题/10 分析篇 | 内存泄漏时,我们该如何一步步找到根因?.md
Normal file
@@ -0,0 +1,245 @@
|
||||
<audio id="audio" title="10 分析篇 | 内存泄漏时,我们该如何一步步找到根因?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/68/412a07e3793675c2134dde65d03f6968.mp3"></audio>
|
||||
|
||||
你好,我是邵亚方。
|
||||
|
||||
通过我们前面的基础篇以及案例篇的学习,你对内存泄漏应该有了自己的一些理解。这节课我来跟你聊一聊系统性地分析内存泄漏问题的方法:也就是说,在面对内存泄漏时,我们该如何一步步去找到根因?
|
||||
|
||||
不过,我不会深入到具体语言的实现细节,以及具体业务的代码逻辑中,而是会从Linux系统上通用的一些分析方法来入手。这样,不论你使用什么开发语言,不论你在开发什么,它总能给你提供一些帮助。
|
||||
|
||||
## 如何定位出是谁在消耗内存 ?
|
||||
|
||||
内存泄漏的外在表现通常是系统内存不够,严重的话可能会引起OOM (Out of Memory),甚至系统宕机。那在发生这些现象时,惯用的分析套路是什么呢?
|
||||
|
||||
首先,我们需要去找出到底是谁在消耗内存,/proc/meminfo可以帮助我们来快速定位出问题所在。
|
||||
|
||||
/proc/meminfo中的项目很多,我们没必要全部都背下来,不过有些项是相对容易出问题的,也是你在遇到内存相关的问题时,需要重点去排查的。我将这些项列了一张表格,也给出了每一项有异常时的排查思路。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a4/30/a48d1c573d19e30ecee8dc6f6fdd3930.jpg" alt="">
|
||||
|
||||
总之,如果进程的内存有问题,那使用top就可以观察出来;如果进程的内存没有问题,那你可以从/proc/meminfo入手来一步步地去深入分析。
|
||||
|
||||
接下来,我们分析一个实际的案例,来看看如何分析进程内存泄漏是什么原因导致的。
|
||||
|
||||
## 如何去分析进程的内存泄漏原因?
|
||||
|
||||
这是我多年以前帮助一个小伙伴分析的内存泄漏问题。这个小伙伴已经使用top排查出了业务进程的内存异常,但是不清楚该如何去进一步分析。
|
||||
|
||||
他遇到的这个异常是,业务进程的虚拟地址空间(VIRT)被消耗很大,但是物理内存(RES)使用得却很少,所以他怀疑是进程的虚拟地址空间有内存泄漏。
|
||||
|
||||
我们在“[06讲](https://time.geekbang.org/column/article/280455)”中也讲过,出现该现象时,可以用top命令观察(这是当时保存的生产环境信息,部分信息做了脱敏处理):
|
||||
|
||||
```
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
31108 app 20 0 285g 4.0g 19m S 60.6 12.7 10986:15 app_server
|
||||
|
||||
```
|
||||
|
||||
可以看到app_server这个程序的虚拟地址空间(VIRT这一项)很大,有285GB。
|
||||
|
||||
那该如何追踪app_server究竟是哪里存在问题呢?
|
||||
|
||||
我们可以用pidstat命令(关于该命令,你可以[man pidstat](https://linux.die.net/man/1/pidstat))来追踪下该进程的内存行为,看看能够发现什么现象。
|
||||
|
||||
```
|
||||
$ pidstat -r -p 31108 1
|
||||
|
||||
|
||||
04:47:00 PM 31108 353.00 0.00 299029776 4182152 12.73 app_server
|
||||
...
|
||||
04:47:59 PM 31108 149.00 0.00 299029776 4181052 12.73 app_server
|
||||
04:48:00 PM 31108 191.00 0.00 299040020 4181188 12.73 app_server
|
||||
...
|
||||
04:48:59 PM 31108 179.00 0.00 299040020 4181400 12.73 app_server
|
||||
04:49:00 PM 31108 183.00 0.00 299050264 4181524 12.73 app_server
|
||||
...
|
||||
04:49:59 PM 31108 157.00 0.00 299050264 4181456 12.73 app_server
|
||||
04:50:00 PM 31108 207.00 0.00 299060508 4181560 12.73 app_server
|
||||
...
|
||||
04:50:59 PM 31108 127.00 0.00 299060508 4180816 12.73 app_server
|
||||
04:51:00 PM 31108 172.00 0.00 299070752 4180956 12.73 app_server
|
||||
|
||||
```
|
||||
|
||||
如上所示,在每个整分钟的时候,VSZ会增大10244KB,这看起来是一个很有规律的现象。然后,我们再来看下增大的这个内存区域到底是什么,你可以通过/proc/PID/smaps来看(关于/proc提供的信息,你可以回顾我们课程的“[05讲](https://time.geekbang.org/column/article/279307)”):
|
||||
|
||||
增大的内存区域,具体如下:
|
||||
|
||||
```
|
||||
$ cat /proc/31108/smaps
|
||||
...
|
||||
7faae0e49000-7faae1849000 rw-p 00000000 00:00 0
|
||||
Size: 10240 kB
|
||||
Rss: 80 kB
|
||||
Pss: 80 kB
|
||||
Shared_Clean: 0 kB
|
||||
Shared_Dirty: 0 kB
|
||||
Private_Clean: 0 kB
|
||||
Private_Dirty: 80 kB
|
||||
Referenced: 60 kB
|
||||
Anonymous: 80 kB
|
||||
AnonHugePages: 0 kB
|
||||
Swap: 0 kB
|
||||
KernelPageSize: 4 kB
|
||||
MMUPageSize: 4 kB
|
||||
7faae1849000-7faae184a000 ---p 00000000 00:00 0
|
||||
Size: 4 kB
|
||||
Rss: 0 kB
|
||||
Pss: 0 kB
|
||||
Shared_Clean: 0 kB
|
||||
Shared_Dirty: 0 kB
|
||||
Private_Clean: 0 kB
|
||||
Private_Dirty: 0 kB
|
||||
Referenced: 0 kB
|
||||
Anonymous: 0 kB
|
||||
AnonHugePages: 0 kB
|
||||
Swap: 0 kB
|
||||
KernelPageSize: 4 kB
|
||||
MMUPageSize: 4 kB
|
||||
|
||||
```
|
||||
|
||||
可以看到,它包括:一个私有地址空间,这从rw-p这个属性中的private可以看出来;以及一个保护页 ,这从—p这个属性可以看出来,即进程无法访问。对于有经验的开发者而言,从这个4K的保护页就可以猜测出应该跟线程栈有关了。
|
||||
|
||||
然后我们跟踪下进程申请这部分地址空间的目的是什么,通过strace命令来跟踪系统调用就可以了。因为VIRT的增加,它的系统调用函数无非是mmap或者brk,那么我们只需要strace的结果来看下mmap或brk就可以了。
|
||||
|
||||
用strace跟踪如下:
|
||||
|
||||
```
|
||||
$ strace -t -f -p 31108 -o 31108.strace
|
||||
|
||||
```
|
||||
|
||||
线程数较多,如果使用-f来跟踪线程,跟踪的信息量也很大,逐个搜索日志里面的mmap或者brk真是眼花缭乱, 所以我们来grep一下这个大小(10489856即10244KB),然后过滤下就好了:
|
||||
|
||||
```
|
||||
$ cat 31108.strace | grep 10489856
|
||||
31152 23:00:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31157 23:02:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31158 23:03:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31165 23:04:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31163 23:05:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31153 23:06:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31155 23:07:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31149 23:08:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31147 23:09:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31159 23:10:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31157 23:11:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31148 23:12:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31150 23:13:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31173 23:14:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
|
||||
```
|
||||
|
||||
从这个日志我们可以看到,出错的是mmap()这个系统调用,那我们再来看下mmap这个内存的目的:
|
||||
|
||||
```
|
||||
31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0 <unfinished ...>
|
||||
31151 23:01:00 mprotect(0x7fa94bbc0000, 4096, PROT_NONE <unfinished ...> <<< 创建一个保护页
|
||||
31151 23:01:00 clone( <unfinished ...> <<< 创建线程
|
||||
31151 23:01:00 <... clone resumed> child_stack=0x7fa94c5afe50, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND
|
||||
|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID
|
||||
|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fa94c5c09d0, tls=0x7fa94c5c0700, child_tidptr=0x7fa94c5c09d0) = 20610
|
||||
|
||||
```
|
||||
|
||||
可以看出,这是在clone时申请的线程栈。到这里你可能会有一个疑问:既然线程栈消耗了这么多的内存,那理应有很多才对啊?
|
||||
|
||||
但是实际上,系统中并没有很多app_server的线程,那这是为什么呢?答案其实比较简单:线程短暂执行完毕后就退出了,可是mmap的线程栈却没有被释放。
|
||||
|
||||
我们来写一个简单的程序复现这个现象,问题的复现是很重要的,如果很复杂的问题可以用简单的程序来复现,那就是最好的结果了。
|
||||
|
||||
如下是一个简单的复现程序:mmap一个40K的线程栈,然后线程简单执行一下就退出。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#define _SCHED_H
|
||||
#define __USE_GNU
|
||||
#include <bits/sched.h>
|
||||
|
||||
#define STACK_SIZE 40960
|
||||
|
||||
int func(void *arg)
|
||||
{
|
||||
printf("thread enter.\n");
|
||||
sleep(1);
|
||||
printf("thread exit.\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main()
|
||||
{
|
||||
int thread_pid;
|
||||
int status;
|
||||
int w;
|
||||
|
||||
while (1) {
|
||||
void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);
|
||||
if (addr == NULL) {
|
||||
perror("mmap");
|
||||
goto error;
|
||||
}
|
||||
printf("creat new thread...\n");
|
||||
thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS|CLONE_VM|CLONE_FILES, NULL);
|
||||
printf("Done! Thread pid: %d\n", thread_pid);
|
||||
if (thread_pid != -1) {
|
||||
do {
|
||||
w = waitpid(-1, NULL, __WCLONE | __WALL);
|
||||
if (w == -1) {
|
||||
perror("waitpid");
|
||||
goto error;
|
||||
}
|
||||
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
|
||||
}
|
||||
sleep(10);
|
||||
}
|
||||
|
||||
error:
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后我们用pidstat观察该进程的执行,可以发现它的现象跟生产环境中的问题是一致的:
|
||||
|
||||
```
|
||||
$ pidstat -r -p 535 5
|
||||
11:56:51 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
|
||||
11:56:56 PM 0 535 0.20 0.00 4364 360 0.00 a.out
|
||||
11:57:01 PM 0 535 0.00 0.00 4364 360 0.00 a.out
|
||||
11:57:06 PM 0 535 0.20 0.00 4404 360 0.00 a.out
|
||||
11:57:11 PM 0 535 0.00 0.00 4404 360 0.00 a.out
|
||||
11:57:16 PM 0 535 0.20 0.00 4444 360 0.00 a.out
|
||||
11:57:21 PM 0 535 0.00 0.00 4444 360 0.00 a.out
|
||||
11:57:26 PM 0 535 0.20 0.00 4484 360 0.00 a.out
|
||||
11:57:31 PM 0 535 0.00 0.00 4484 360 0.00 a.out
|
||||
11:57:36 PM 0 535 0.20 0.00 4524 360 0.00 a.out
|
||||
^C
|
||||
Average: 0 535 0.11 0.00 4435 360 0.00 a.out
|
||||
|
||||
```
|
||||
|
||||
你可以看到,VSZ每10s增大40K,但是增加的那个线程只存在了1s就消失了。
|
||||
|
||||
至此我们就可以推断出app_server的代码哪里有问题了,然后小伙伴去修复该代码Bug,很快就把该问题给解决了。
|
||||
|
||||
当然了,应用程序的内存泄漏问题其实是千奇百怪的,分析方法也不尽相同,我们讲述这个案例的目的是为了告诉你一些通用的分析技巧。我们掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。
|
||||
|
||||
## 课堂总结
|
||||
|
||||
这节课我们讲述了系统性分析Linux上内存泄漏问题的分析方法,要点如下:
|
||||
|
||||
- top工具和/proc/meminfo文件是分析Linux上内存泄漏问题,甚至是所有内存问题的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;
|
||||
- 应用程序的内存泄漏千奇百怪,所以你需要掌握一些通用的分析技巧,掌握了这些技巧很多时候就可以以不变应万变。但是,这些技巧的掌握,是建立在你的基础知识足够扎实的基础上。你需要熟练掌握我们这个系列课程讲述的这些基础知识,熟才能生巧。
|
||||
|
||||
## 课后作业
|
||||
|
||||
请写一个内存泄漏的程序,然后观察/proc/[pid]/maps以及smaps的变化(pid即内存泄漏的程序的pid)。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲见。
|
||||
Reference in New Issue
Block a user