跳到主要内容

内存

内存基础

进程申请内存类型汇总​

  • 私有匿名内存。进程的堆、栈,以及 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 就属于这类内存。

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 中字段的对应关系:

topstatm含义
VIRTsize进程申请的虚拟内存大小
RESresident映射到进程地址空间的物理内存大小
SHRshared以 MAP_SHARED 方式映射的内存,可能被共享
CODEtext代码段大小
DATAdata数据段大小
nDRTdt脏页大小(从 2.6 之后就废弃了,一直为 0)

有些时候所有进程的 RES 相加起来要比系统总的物理内存大,这是因为 RES 中有一些内存是被一些进程给共享的。

如果想要继续看某个进程的内存使用细节,可以使用 pmap。

$ 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 ]
...

每一列的含义如下:

  • 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 同样也是解析的 /proc 里的文件,具体文件是 /proc/[pid]/maps 和 /proc/[pid]/smaps,其中 smaps 文件相比 maps 的内容更详细,可以理解为是对 maps 的一个扩展。

内核内存泄漏​

应用程序可以通过 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 则是不可以被回收只能主动释放的内存。

内核空间内存的生命周期是与内核一致的,却不是跟内核模块一致的,也就是说,在内核模块退出时,不会自动释放掉该内核模块申请的内存,只有在内核重启(即服务器重启)时才会释放掉这部分内存。

内核内存泄漏的问题往往会发生在一些驱动程序中,比如说网卡驱动,SSD 卡驱动等,以及我们自己开发的一些驱动,因为这类驱动不像 Linux 内核那样经历过大规模的功能验证和测试,所以相对容易出现一些隐藏很深的问题。

观测内核内存泄漏​

  • 如果 /proc/meminfo 中内核内存(比如 VmallocUsed 和 SUnreclaim)太大,那很有可能发生了内核内存泄漏;
  • 周期性地观察 VmallocUsed 和 SUnreclaim 的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏。

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”,我们需要在使用完该内存空间后就释放它以节省内存。

使能这些 tracepoints 后,也可以观察内存的动态申请和释放情况,只是这个分析过程不如 kmemleak 那么高效。

内存类型Tracepoints
kmalloc、kfreekmalloc、kfree
kmem_cachekmem_cache_alloc、kmem_cache_free
pagemm_page_alloc、mm_page_free

内存泄漏排查​

alt text