首页 > 数据库 > 数据库开发技术 > 从pmap内核实现原理聊聊如何分析内存
上周五帮同事看了一个内存泄漏 mem leak 的问题,简而言之,就是使用 pmap -d 发现 writeable/private 的内存持续上涨。
赶巧,今晚又有一位朋友私信我,怀疑也是某数据库内存泄露了:

借此机会,我也看了一下 pmap -d 的内核代码,简单聊聊。
内存的分类很多,可以细分为
至于私有和共享又可以进一步划分:


因此我们在分析内存的时候,要分清楚到底是共享内存还是私有内存,是不是匿名页,也要搞清 PSS/USS/RSS/VSS 这类指标的差异:
我经常使用的是 TOP(RSS,因此 TOP 分析内存是不准的)、pmap、smem、pidstat、sar 等,都可以用于分析内存。此文让我们聚焦于 pmap。
pmap 用于显示进程的内存映像,显式每个 vm_area_struct 指向的虚拟内存区域,所指向的虚拟内存区域又可以细分为代码段、数据段、堆、内存映射区、栈等等 (这块不熟悉的自行谷歌)。
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/*
* Access permissions of this VMA.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
void * vm_private_data; /* was vm_pte (shared mem) */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
}
每一个VMA都有如下的一系列数据,我们可以通过 /proc/xxx/smaps 进行查看。
08048000-080bc000 r-xp 00000000 03:02 13130 /bin/bash
Size: 1084 kB
Rss: 892 kB
Pss: 374 kB
Shared_Clean: 892 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 892 kB
Anonymous: 0 kB
ShmemPmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd ex mr mw me dw
此处我们重点看下 vm_flags,用于标记这块虚拟内存区域的行为规范,此处我们只关注如下几个
比如代码段这块内存区域的权限是可读,可执行,但是不可写。数据段具有可读可写的权限但是不可执行。堆则具有可读可写,可执行的权限。栈一般是可读可写的权限,一般很少有可执行权限。而文件映射与匿名映射区存放了共享链接库,所以也需要可执行的权限。
因此回到 pmap,这个命令背后的实现原理就是通过遍历内核中的这个 vm_area_struct 获取的。那让我们再详细剖析下 pmap -d 的内核实现原理是怎样的:

可以看到,pmap 就是读取的 /proc/进程号/maps 文件进行计算的,并且 writable/private 的逻辑也很简单
那让我们模拟一下,一段简短的内存泄露代码,没有及时 FREE。
#include <stdlib.h>
#include <unistd.h>
int main() {
int run_time = 50 * 60;
for (int i = 0; i < run_time; ++i) {
char *ptr = (char*)malloc(1024 * sizeof(char));
// free(ptr);
sleep(1);
}
return 0;
}
运行起来,然后观察其内存使用状况,不一会儿就在缓慢上涨了
[postgres@xiongcc ~]$ while true; do pmap -d 27647 | tail -1; sleep 2; done
mapped: 4348K writeable/private: 320K shared: 0K
mapped: 4348K writeable/private: 320K shared: 0K
mapped: 4348K writeable/private: 320K shared: 0K
mapped: 4348K writeable/private: 320K shared: 0K
...
mapped: 4612K writeable/private: 584K shared: 0K
mapped: 4612K writeable/private: 584K shared: 0K
...
mapped: 4744K writeable/private: 848K shared: 0K
mapped: 4744K writeable/private: 848K shared: 0K
同时让我们检查一下 smaps
[postgres@xiongcc ~]$ cat /proc/27647/smaps | egrep -w 'Size:'
Size: 4 kB
Size: 4 kB
Size: 4 kB
Size: 660 kB
Size: 1808 kB
Size: 2044 kB
Size: 16 kB
Size: 8 kB
Size: 20 kB
Size: 136 kB
Size: 12 kB
Size: 4 kB
Size: 4 kB
Size: 4 kB
Size: 4 kB
Size: 132 kB
Size: 8 kB
Size: 4 kB
[postgres@xiongcc ~]$ pmap -d 27647 | egrep -v '27647|Address|mapped' | cat -n
1 0000000000400000 4 r-x-- 0000000000000000 0fd:00001 mem_leak
2 0000000000600000 4 r---- 0000000000000000 0fd:00001 mem_leak
3 0000000000601000 4 rw--- 0000000000001000 0fd:00001 mem_leak
4 00000000015dd000 660 rw--- 0000000000000000 000:00000 [ anon ]
5 00007f5af2b7d000 1808 r-x-- 0000000000000000 0fd:00001 libc-2.17.so
6 00007f5af2d41000 2044 ----- 00000000001c4000 0fd:00001 libc-2.17.so
7 00007f5af2f40000 16 r---- 00000000001c3000 0fd:00001 libc-2.17.so
8 00007f5af2f44000 8 rw--- 00000000001c7000 0fd:00001 libc-2.17.so
9 00007f5af2f46000 20 rw--- 0000000000000000 000:00000 [ anon ]
10 00007f5af2f4b000 136 r-x-- 0000000000000000 0fd:00001 ld-2.17.so
11 00007f5af315e000 12 rw--- 0000000000000000 000:00000 [ anon ]
12 00007f5af316b000 4 rw--- 0000000000000000 000:00000 [ anon ]
13 00007f5af316c000 4 r---- 0000000000021000 0fd:00001 ld-2.17.so
14 00007f5af316d000 4 rw--- 0000000000022000 0fd:00001 ld-2.17.so
15 00007f5af316e000 4 rw--- 0000000000000000 000:00000 [ anon ]
16 00007fff958b5000 132 rw--- 0000000000000000 000:00000 [ stack ]
17 00007fff95925000 8 r-x-- 0000000000000000 000:00000 [ anon ]
18 ffffffffff600000 4 r-x-- 0000000000000000 000:00000 [ anon ]
可以看到,pmap -d 本质上就是将 smaps 文件的 VMA 取了出来,那让我们手动计算一下,看看是不是 848 KB。
Address: 00400000-00401000, Permissions: r-xp, Size: 4, Path: /home/postgres/mem_leak
Address: 00600000-00601000, Permissions: r--p, Size: 4, Path: /home/postgres/mem_leak
Address: 00601000-00602000, Permissions: rw-p, Size: 4, Path: /home/postgres/mem_leak
Address: 015dd000-01682000, Permissions: rw-p, Size: 660, Path: [heap] ---内存大头
Address: 7f5af2b7d000-7f5af2d41000, Permissions: r-xp, Size: 1808, Path: /usr/lib64/libc-2.17.so
Address: 7f5af2d41000-7f5af2f40000, Permissions: ---p, Size: 2044, Path: /usr/lib64/libc-2.17.so
Address: 7f5af2f40000-7f5af2f44000, Permissions: r--p, Size: 16, Path: /usr/lib64/libc-2.17.so
Address: 7f5af2f44000-7f5af2f46000, Permissions: rw-p, Size: 8, Path: /usr/lib64/libc-2.17.so
Address: 7f5af2f46000-7f5af2f4b000, Permissions: rw-p, Size: 20, Path: 0
Address: 7f5af2f4b000-7f5af2f6d000, Permissions: r-xp, Size: 136, Path: /usr/lib64/ld-2.17.so
Address: 7f5af315e000-7f5af3161000, Permissions: rw-p, Size: 12, Path: 0
Address: 7f5af316b000-7f5af316c000, Permissions: rw-p, Size: 4, Path: 0
Address: 7f5af316c000-7f5af316d000, Permissions: r--p, Size: 4, Path: /usr/lib64/ld-2.17.so
Address: 7f5af316d000-7f5af316e000, Permissions: rw-p, Size: 4, Path: /usr/lib64/ld-2.17.so
Address: 7f5af316e000-7f5af316f000, Permissions: rw-p, Size: 4, Path: 0
Address: 7fff958b5000-7fff958d6000, Permissions: rw-p, Size: 132, Path: [stack]
Address: 7fff95925000-7fff95927000, Permissions: r-xp, Size: 8, Path: [vdso]
Address: ffffffffff600000-ffffffffff601000, Permissions: r-xp, Size: 4, Path: [vsyscall]
按照前面的逻辑,p → w → 相加,那么就是 4 + 660 + 8 + 20 + 12 + 4 + 4 + 4 + 132 = 848 KB,可以看到,对得上了。
注意此处的 Path:0 因为是 shell 的原因,以 08048000-080bc000 r-xp 00000000 03:02 13130 /bin/bash 为例,分别表示
如果是无显示的情况下,我问了一下 GPT

以我这段简短的 C 代码为例,其计算原理也就明白了:将私有的(排除了共享库),可写的(排除只读代码段、包括数据段、栈、堆)相加,得到私有内存。和代码中的原理相同。
/* the permission string looks like "rwxp", where each character can
* be either the letter, or a hyphen. The final character is either
* p for private or s for shared. We want to add up private writable
* mappings, to get a feel for how much private memory this process
* is taking.
*
* Also, we add up the shared mappings, to see how much this process
* is sharing with others.
*/
此例中内存大头便是堆 heap,因为我一直在源源不断地申请内存;其次也将 /usr/lib64/libc-2.17.so 计算了进去。
以上便是 pmap -d 的实现原理,通过这个例子,想必各位对于 Linux 复杂的内存机制会有更多的思考。另外,smem 工具也是一款推荐查看内存的工具,这二者的统计方式也存在较大差异,下面是 USS/PSS/RSS,可以看到 smem 是将 USS 缓慢累计的,计算方式必然有所不同。

后面找时间再分析一下 smem 的实现原理。That's all!
https://techtalk.intersec.com/2013/07/memory-part-2-understanding-process-memory/
进程的smaps内存使用分析
Linux内存管理 -- /proc/{pid}/smaps讲解
原文作者:PostgreSQL学徒;如有侵权,请联系管理员删除。