原文地址:https://techtalk.intersec.com/2013/07/memory-part-2-understanding-process-memory/ node
在前一篇文章,咱们介绍了一个方法来划分进程用到的内存。咱们使用了两个纬度获得四个类别:私有/共享,匿名/基于文件。咱们引入了共享机制的复杂度和全部内存都由内核分配回收的事实。 linux
全部咱们谈到的都是虚拟地址。全部关于内存地址的分配,内核并不老是当即把分配的地址映射到物理内存。大部分时候,内核老是延迟到对该地址的第一次访问(某些状况下是第一次写)才分配实际的物理内存。而且这个分配的粒度是以页(4KB)为单位。更进一步,一些页在分配后可能被交换出去,这意味着写到磁盘,从而可让其余的页放入物理内存。 git
所以,想知道进程实际使用的物理内存(进程的resident内存)大小很是困难。可是内核做为系统的一个独立模块可以知道这个数据(这实际上也是它的其中一个工做)。幸运的是,内核提供了一些接口来让你得到系统和特定进程的一些统计数据。本文会深刻了解Linux系统提供的工具,用于分享进程的内存模式。 程序员
在Linux上,那些统计数据经过/proc文件系统暴露出来,特别是/proc/[pid]下面的内容。这些目录(每一个进程一个)包含一些伪文件,他们是直接获取内核信息的API。关于/proc目录的内容,更详细的信息能够查看proc(5)手册页(每一个Linux版本的内容都会有一些改变)。 shell
如procps(ps, top, pmap ...)这样的工具调用那些API并提供了人易于阅读的信息。这些工具把从内核取到的信息作少许的修改,或者彻底不修改并输出。所以他们是一个很好的入口点来理解内核是如何对内存进行分类的。在这篇文章咱们将分析top和pmap命令跟内存相关的输出。 小程序
top是一个广为人知(并使用)的用于系统监控的工具。它在每一行用可变的列显示一个进程的相关信息,能够是关于CPU的,关于内存的或者其余更加通用的信息。 后端
当top运行时,你能够按G3切换到内存视图。在这个试图,你会看到其中包含这些列:%MEM, VIRT, SWAP, RES, CODE, DATA, SHR。除了SWAP,全部的数据都是从/proc/[pid]/statm文件获取的。这个文件暴露了一些内存相关的统计数据。它包含了7个数字字段:size(在输出中映射为VIRT),resident(映射为RES),shared(映射为SHR),text(映射为CODE),lib(Linux 2.6以上老是0),data(映射为DATA)以及dt(Linux 2.6以上老是0,映射为nDrt)。 app
正如你猜测的那样,有些列很容易理解。VIRT是进程到目前位置分配的总虚拟地址空间大小。CODE是当前执行的二进制文件可执行代码的大小。RES是实际占用的内存大小,也就是内核认为分配给进程的物理内存的总数。所以%MEM是由RES计算得出的。 ide
实际内存的大小是内核把两个计数器相加所得。第一个包含匿名物理内存页(MM_ANONPAGES)的数量。另外一个是基于文件的内存页(MM_FILEPAGES)数量。一些页可能被认为同时被多个进程占用,所以RES的和可能比实际使用的物理内存大,甚至可能比系统可用的物理内存总量还大。 工具
SHR是进程实际使用的共享物理内存总数。若是你还记得咱们上一篇文章对内存的分类,你能够假定它包含全部右边一列(注,上一篇2X2表格的右边一列中的两项)实际占用的内存。可是咱们已经讨论过,有些私有内存也会被共享。所以,为了更好地理解这一列的实际含义,咱们要更加深刻的了解一下内核。
SHR列是用/proc/[pid]/statm中shared字段的值填充获得的。shared字段自己是内核中MM_FILEPAGES计数器的值,也是统计实际占用内存大小的两个计数器之一。这仅仅意味着这一列包含基于文件的实际占用内存(也所以包含类别3和4)。
这很酷。。。可是回想类别2:共享的匿名内存不存在。。。前面的定义只包含共享的基于文件的内存。而后运行如下程序的结果代表,共享的匿名内存被计入SHR列。
#include <sys/mman.h> #include <unistd.h> #include <stdint.h> int main() { /* mmap 50MiB 共享的匿名内存 */ char *p = mmap(NULL, 50 << 20, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); /* 访问每一页使得它们调入内存 */ for (int i = 0; i < (50 << 20) / 4096; i++) { p[i * 4096] = 1; } /* 停一下,咱们能够有时间去看top */ sleep(1000000); return 0; }
这是由Linux内核形成的。在Linux上共享匿名map其实是基于文件的。内核在tmpfs上建立一个文件(/dev/zero的一个实例)。这个文件被当即unlink掉,所以没法被其余进程访问,除非它们继承了这个map(经过fork)。这个作法很是聪明,由于这个共享经过文件层,也就和基于文件的共享映射使用同样的方法(第4类)。
最后一点,因为基于文件的私有内存页再被改动后不会同步回磁盘,它们就再也不是基于文件的(它们被从MM_FILEPAGES计数器转移到MM_ANONPAGES计数器)。所以,它们再也不被统计到SHR中。
注意top的man手册中的错误,它说SHR可能包含非占用内存:the amount of shared memory available to a task, not all of which is typically resident. 它仅仅反映那些有可能被其余进程共享的内存.。
DATA列的含义是很是模糊的。top的文档说是“data+stack”。但这没有任何帮助,由于它仍是没有定义“Data"。如今咱们仍是要再次深刻内核。
内核经过计算两个变量的差来得到DATA字段:total_vm(至关于VIRT)和shared_vm。shared_vm跟SHR很相似,他们都有可共享的内存的含义。可是它不只计算实际占用内存,它包含全部可寻址的基于文件的内存。更重要的是,这个计数是在映射层面而不是页的层面。所以shared_vm没有引入SHR关于私有的基于文件的内存的微妙技法。因此shared_vm包含类别2,3和4的合。这意味着total_vm和shared_vm的差刚好是类别1的数量。
DATA列包含已分配的全部私有匿名内存的总数。根据定义,私有匿名内存是进程特有的而且存放了进程数据。它仅能经过fork的写时拷贝功能被共享。它包含(但不限于)栈和堆。这列不包含任何进程实际占用内存的信息,它仅仅告诉咱们进程分配的内存总数,可是这些内存可能很长时间都彻底没有用到。
能够经过一个典型例子的证实DATA的值是毫无心义的:看一下用Address Sanitizer工具编译的x86_64程序启动时发生了什么。ASan工做时分配了16TiB的内存,可是仅仅使用了进程分配的内存中每8个字节(64位的一个word)中的一个字节。结果top的输出以下:
3 PID %MEM VIRT SWAP RES CODE DATA SHR COMMAND 16190 0.687 16.000t 0 56056 13784 16.000t 2912 zchk-asan
SWAP某种意义上跟其余都不一样。这列照理包含全部进程被内核交换出去的内存的总和。首先,这列的内容彻底依赖于Linux和top的版本。在Linux2.6.34以前内核没有暴露任何按进程统计的交换出去的页的数量。top3.3.0以前显示的是彻底无心义的数据(可是和man手册保持一致)。然而,若是你使用Linux2.6.34及top3.3.0之后的版本,这个数量是实际交换出去的页的数量。
若是你的top太老,SWAP列填入的是VIRT和RES的差。这彻底没有意义,由于这个差值实际上表示全部交换出去的内存总数,可是它也包含全部基于文件的未加载的页和分配的可是未使用的页(所以尚未实际分配)。一些Linux发行版仍然使用有这个SWAP信息不对的top版本,其中还在被普遍使用的有REHL5。
若是你的top已经更新到最新版本,可是你的内核太老。这一列老是0,这彻底没用。
若是你的内核跟top都是最新的,那么这一列包含文件/proc/[pid]/status中VmSwap的值。它由内核维护,是一个计数器,每次在一个页被交换出去时增长,换进时减小。所以它是准确的,能够提供给你一个重要信息:基本上,若是这个值非0,说明你的系统有一些内存的压力,你的进程使用的内存不能在物理内存中所有放下。
man手册描述SWAP为任务的地址空间中不占用物理内存的部分,这是top3.3.0以前的实现,可是没有提到实际被交换出去的内存总数。在更早版本的top手册中正确解释了输出了什么,可是SWAP的名称并不合适。
pmap是另一种工具。它比top更加深刻的显示了进程的每个内存映射信息。在这个视图里,一个映射是一个包含连续页的范围,它们有相同的后端(匿名或文件)和相同的访问模式。
对每个映射,pmap显示前面列出的选项和映射的大小,实际占用页的总数和脏页的总数。脏页是指已经被改写过可是尚未同步到对应文件的页。所以,脏页的数量只对须要回写的映射有意义,即共享的基于文件的映射(类别4)。
pmap的数据源是两我的可阅读的文件:/proc/[pid]/maps和/proc/[pid]/smaps。第一个文件只是简单列出了映射,第二个文件对每一个映射用一个单独的段列出了更详细的信息。Linux2.6.14开始支持smaps,这个版本已经很老,所以目前流行的发行版都支持。
pmap的用法很简单:
pmap工具是受到Solaris系统上相似工具的启发,而且模仿其行为。这里是一个测试共享匿名内存小程序,咱们打印pmap的输出和/proc/[pid]/maps文件的内容:
3009: ./blah 0000000000400000 4K r-x-- /home/fruneau/blah 0000000000401000 4K rw--- /home/fruneau/blah 00007fbb5da87000 51200K rw-s- /dev/zero (deleted) 00007fbb60c87000 1536K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fbb60e07000 2048K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fbb61007000 16K r---- /lib/x86_64-linux-gnu/libc-2.13.so 00007fbb6100b000 4K rw--- /lib/x86_64-linux-gnu/libc-2.13.so 00007fbb6100c000 20K rw--- [ anon ] 00007fbb61011000 128K r-x-- /lib/x86_64-linux-gnu/ld-2.13.so 00007fbb61221000 12K rw--- [ anon ] 00007fbb6122e000 8K rw--- [ anon ] 00007fbb61230000 4K r---- /lib/x86_64-linux-gnu/ld-2.13.so 00007fbb61231000 4K rw--- /lib/x86_64-linux-gnu/ld-2.13.so 00007fbb61232000 4K rw--- [ anon ] 00007fff9350f000 132K rw--- [ stack ] 00007fff9356e000 4K r-x-- [ anon ] ffffffffff600000 4K r-x-- [ anon ] total 55132K
00400000-00401000 r-xp 00000000 08:01 3507636 /home/fruneau/blah 00401000-00402000 rw-p 00000000 08:01 3507636 /home/fruneau/blah 7fbb5da87000-7fbb60c87000 rw-s 00000000 00:04 8467 /dev/zero (deleted) 7fbb60c87000-7fbb60e07000 r-xp 00000000 08:01 3334313 /lib/x86_64-linux-gnu/libc-2.13.so 7fbb60e07000-7fbb61007000 ---p 00180000 08:01 3334313 /lib/x86_64-linux-gnu/libc-2.13.so 7fbb61007000-7fbb6100b000 r--p 00180000 08:01 3334313 /lib/x86_64-linux-gnu/libc-2.13.so 7fbb6100b000-7fbb6100c000 rw-p 00184000 08:01 3334313 /lib/x86_64-linux-gnu/libc-2.13.so 7fbb6100c000-7fbb61011000 rw-p 00000000 00:00 0 7fbb61011000-7fbb61031000 r-xp 00000000 08:01 3334316 /lib/x86_64-linux-gnu/ld-2.13.so 7fbb61221000-7fbb61224000 rw-p 00000000 00:00 0 7fbb6122e000-7fbb61230000 rw-p 00000000 00:00 0 7fbb61230000-7fbb61231000 r--p 0001f000 08:01 3334316 /lib/x86_64-linux-gnu/ld-2.13.so 7fbb61231000-7fbb61232000 rw-p 00020000 08:01 3334316 /lib/x86_64-linux-gnu/ld-2.13.so 7fbb61232000-7fbb61233000 rw-p 00000000 00:00 0 7fff9350f000-7fff93530000 rw-p 00000000 00:00 0 [stack] 7fff9356e000-7fff9356f000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
每个映射都有相关的一个模式的集合:
前面三个标志能够经过mprotect(2)系统调用设置,也能够在mmap调用时直接设置。
最后一列是数据的来源。在咱们的例子中,咱们能够看到pmap没有保留内核特定的细节。有三类内存:anon,stack和基于文件的(有一个文件路径,若是文件被unlink会显示deleted)。除了这些类别,内核还有vsdo,vsyscall和heap类别。pmap没有保留heap标识,这真是一种耻辱,由于这对程序员来讲很是重要(但这极可能是为了跟Solaris上的命令保持兼容)。
考虑最后一列,咱们能够看见可执行文件和共享库被映射为私有的(但咱们前面的文章已经讨论过这并不许确)而且同一个文件的不一样部分被分别映射(一些部分甚至被映射不止一次)。这是由于可执行文件包含不一样的段(注,这里的段指section,跟内存的段segment并非一回事,但均可以理解一个范围):text,data,rodata,rss...每个段有不一样的含义,而且被分开映射。下一篇文章咱们将讨论这些段。
最后(但不是最少),咱们能够看到咱们的共享匿名内存实际上由一个/dev/zero的拷贝的映射来实现的,它已经被unlink。
pmap -x的内容包含两个额外的列:
Address Kbytes RSS Dirty Mode Mapping 0000000000400000 4 4 4 r-x-- blah 0000000000401000 4 4 4 rw--- blah 00007fc3b50df000 51200 51200 51200 rw-s- zero (deleted) 00007fc3b82df000 1536 188 0 r-x-- libc-2.13.so 00007fc3b845f000 2048 0 0 ----- libc-2.13.so 00007fc3b865f000 16 16 16 r---- libc-2.13.so 00007fc3b8663000 4 4 4 rw--- libc-2.13.so 00007fc3b8664000 20 12 12 rw--- [ anon ] 00007fc3b8669000 128 108 0 r-x-- ld-2.13.so 00007fc3b8879000 12 12 12 rw--- [ anon ] 00007fc3b8886000 8 8 8 rw--- [ anon ] 00007fc3b8888000 4 4 4 r---- ld-2.13.so 00007fc3b8889000 4 4 4 rw--- ld-2.13.so 00007fc3b888a000 4 4 4 rw--- [ anon ] 00007fff7e6ef000 132 12 12 rw--- [ stack ] 00007fff7e773000 4 4 0 r-x-- [ anon ] ffffffffff600000 4 0 0 r-x-- [ anon ] ---------------- ------ ------ ------ total kB 55132 51584 51284
第二个新的列是Dirty。对于共享的基于文件的映射,内核在以为须要释放一些物理内存或脏页太多时会把脏页写回对应的文件。这时脏页会被标识为感受的。对于其余的内存类型,后端要么是匿名的(没有文件后端),要么是私有的(改变对其余进程不可见),经过写到交换分区来卸载这些页。
这只是是内核暴露的信息的一个子集。smaps文件中存放了更多的信息(这使得输出过于冗长而很难阅读)。
00400000-00401000 r-xp 00000000 08:01 3507636 /home/fruneau/blah Size: 4 kB Rss: 4 kB Pss: 4 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 4 kB Private_Dirty: 0 kB Referenced: 4 kB Anonymous: 0 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB 00401000-00402000 rw-p 00000000 08:01 3507636 /home/fruneau/blah Size: 4 kB Rss: 4 kB Pss: 4 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 4 kB Referenced: 4 kB Anonymous: 4 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB 7f55c4dd2000-7f55c7fd2000 rw-s 00000000 00:04 8716 /dev/zero (deleted) Size: 51200 kB Rss: 51200 kB Pss: 51200 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 51200 kB Referenced: 51200 kB Anonymous: 0 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB
你能够看到,理解top和其余工具的输出须要一些操做系统的知识。即便top在各类系统上都有,在它运行的系统上的每一个版本都有一些特别的地方。例如在OS X上你不会看到RES,DATA,SHR等列,而是有RPRVT,RSHRD,RSIZE,VPRVT,VSIZE(注意比Linux上的名称清晰一些)。若是你想更深刻的了解Linux的内存管理,你能够阅读http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/mm或者学习[Understand the Linux Kernel]。
由于本篇有点长,这里作个总结:
若是你喜欢用htop,它的内容和top是彻底同样的。它的man手册也是错误的,或者至少是不清楚的。