2014年(14)
分类: LINUX
2014-04-14 11:00:15
从虚拟内存到物理内存
在上篇文章中,我们介绍了一种进程所占内存分类的方法。通过2个坐标轴划分了4个象限:私有/共享和匿名/文件支持。我们也介绍了共享机制的复杂性和所有内存基本上是由内核回收的事实。
这里我们谈论的都是虚拟内存。所有都是关于(虚拟)内存地址的申请,但申请的地址并不总是立即被内核映射到物理内存。大部分时候,内核会延迟实际物理内存的分配,直到在第一次访问(或第一次写时)……并且分配是以页为单位(每页大小通常是4KB)。此外,一些页面被分配后可能会被换出,这意味着它们会被写入磁盘,以便让其他页面载入RAM中。
因此,想要知道一个进程实际所使用的物理内存(进程的常驻内存)大小真的很困难……内核是系统内真正知道占用内存大小的唯一组件(这可以说是它的一项工作)。幸运的是,内核提供了一些接口可以让你检索到系统或特定进程的一些统计信息。本文将会深入Linux系统工具来分析进程的内存模式。
在Linux上,这些数据通过/proc文件系统提供,/proc/[pid]/中的内容提供了更具体的信息。这些目录(每个进程一个)包含一些伪文件,这些文件是直接从内核检索信息的API入口点。(译者注:proc文件系统是一个伪文件系统。它只存在内存当中,并以文件系统的方式为访问系统内核数据的操作提供接口。)/proc目录内容的详细说明可参考proc(5)手册(在不同版本的linux系统中此内容会有变化)。
procps(ps,top,pmap…)等工具调用这些API,并提供友好的前端显示。这些工具显示从内核检索到的数据,并几乎不做修改。因此它们是理解内核如何划分内存的切入点。在这篇文章中,我们将分析top和pmap中与内存相关的输出。
top:进程统计信息
top是一个众所周知(和使用)的工具,它可以监视系统运行状态。它为每个进程信息显示一行,每一行中包含不同的列,其中包含与CPU、内存相关的信息,或者更一般的信息。
当运行top时 ,按g3可以切换到内存视图 。在那个视图中,你会发现如下几列: %MEM , VIRT , SWAP, RES , CODE ,DATA ,SHR 。除了SWAP ,所有这些数据都是从文件/proc/[pid]/statm中提取,/proc/[pid]/statm提供了这些内存相关的统计数据。该文件包含7个数值域:虚拟内存大小(对应VIRT), 常驻内存 (对应RES ), 共享区(对应SHR ), 代码段(对应CODE), 库(在Linux2.6及以上版本始终为0), 数据段(映射到DATA )和dt (在Linux 2.6及以上版本始终为0 ,对应nDrt )。
一些普通的列
你可能已经猜到,其中一些列很容易理解。VIRT是目前为止为进程分配的虚拟地址空间总大小。CODE是该进程二进制可执行代码段的大小。RES是常驻内存大小,也就是内核为进程分配的物理内存大小。它产生的直接影响就是%MEM与RES严格成正比 。
内核通过两个计数器的和来计算常驻内存大小。第一个包含匿名存储页( MM_ANONPAGES )的数目,第二个是文件存储页(MM_FILEPAGES )的数目。某些页可能被多个进程共享,所以RES的总和可能大于正在被用的RAM,甚至大于系统中可用的RAM。
共享内存
SHR是进程的常驻共享内存大小。如果你还记得我们在上一篇文章中提出的分类,你可以认为它包括右列中的所有常驻内存。前面也讨论过一些私有内存也可以共享。因此,为了了解该列的实际意义,我们必须深入挖掘内核。
SHR展示的是 /proc/[pid]/statm中的shared字段,这个字段是内核MM_FILEPAGES计数器的值,是两个常驻内存计数器之一。这意味着该列包含了基于文件的常驻内存大小(因此包括象限3和4)。
很好……但是先回忆下象限2:共享的匿名内存确实存在…而前面的定义只包括文件支持的内存,并且运行下面的测试程序显示共享的匿名内存是计算到SHR内的:
top的输出显示:RES和SHR列都是50m。
这是由于Linux内核的一个小技巧导致的。在Linux中,一个共享的匿名映射实际上是基于文件的。内核使用tmpfs( /dev/zero的实例)创建文件。该文件会被立即解除链接,因此它不能被其它进程访问,除非它们继承了映射(通过forking)。这相当聪明的:通过和第4象限的文件映射同样的方式实现了共享。
最后一点,被修改的私有文件支持页不会被同步到磁盘,因此它们不再是文件支持型(由MM_FILEPAGES计数器转移到 MM_ANONPAGES)。因此,他们也不再会被统计到SHR。译者注:将上面示例中的MAP_SHARED修改为MAP_PRIVATE,你将看到不同!
需要注意的是top的手册有错误,它声称SHR可能包含非常驻内存:一个任务可用的共享内存并不是所有的都是常驻的。它只是简单反映了那些可能与其他进程共享的内存 。译者注:共享内存会被统计在常驻内存中。
Data
DATA列的含义相当不透明。top命令文档中声明它代表“数据+堆栈”……这并不能提供太多帮助,因为它其实没有定义“数据”。因此,我们得再次深挖内核。
这个字段由内核计算,它是total_vm(与VIRT一致)和shared_vm这两个变量的差值。 shared_vm 与SHR在共享内存的定义有点相似,但与SHR仅包含常驻部分不同,它包含了所有可寻址的文件支持的内存总和。此外,它是在映射级做统计,而不是依据物理页面是否分配,因此shared_vm不具有像SHR中修改私有文件支持内存的小技巧。因此shared_vm是2,第3和第4象限的总和。这意味着,total_vm和shared_vm间的差就是象限1的内容。
DATA列包含申请的私有匿名内存。根据定义,私有匿名内存是程序所特有用来保存其数据的内存。它只能通过forking的写时拷贝机制来共享。它包括但并不限于栈和堆 。此列不包含程序实际使用内存大小的任何信息,它只是告诉我们,程序申请了一定量的内存,但这段内存可能在很长一段时间内不会被修改。
查看DATA值的一个典型例子是观察编译时带的x86_64位程序的启动。地址合法性检查会映射16TiB的内存,但对于实际分配给进程的内存每8字节只使用1个字节。因此, top的输出看起来像这样:
请注意top 的帮助文档再次出现错误:它声明DATA是物理内存使用的大小而不是可执行代码,也被称为“常驻数据集“或DRS,但我们刚看到DATA 和常驻内存一点关系都没有。
swap
SWAP不同于其他列。该列通常被认为包含进程被内核交换出去的内存大小。首先,该列的内容完全取决于Linux版本和你正在运行的top版本。在Linux 2.6.34之前,内核没有公开任何进程换出页面数的统计。在top 3.3.0版本前, top在这一列显示的是完全没有意义的信息(不过与手册一致)。不过,如果你使用Linux 2.6.34或更高版本,带有top的3.3.0或以上版本,显示的计数实际上就是换出的页面的数量了。
如果你的top版本太老, SWAP列显示的是VIRT和RES的差。这是完全没有意义的,因为这个差值实际上既包括已换出的内存,也包括被卸载的文件支持页或者是那些申请了但未改变的(因而没有被实际分配)页。在一些老的Linux发行版中,仍然包含SWAP错误的top工具,这些版本包括仍被广泛使用的RHEL5。
如果你的top是最新的,但你的内核太老,该列将始终为0,这并不能给你带来什么帮助。
如果你的内核和top工具都是最新的,那么该列将包含文件/proc/[pid]/status中VmSwap字段的值 。这个计数器由内核维护,每次页面被换出时增加,每次页面换入时减少。因此,它是准确的,并会为你提供重要信息:基本上如果该值非0,这意味着系统内存吃紧,RAM中无法容纳进程所需的内存。
在man中描述SWAP 为一个任务地址空间中的非驻留部分,但与已换出的实际内存无关,这在top 3.3.0版本之前已经实现。在早期版本的top中 ,man手册正确的解释了显示的内容,不过用SWAP命名不恰当。
pmap:详细的映射
pmap是另一个工具。通过显示进程的每个独立映射,它比top显示的了更深层的信息。在它的显示中,映射是一系列具有相同后端(匿名或文件)和相同访问模式的连续页面。
对于每个映射,pmap既显示先前列出的选项也显示了映射的大小,常驻页的大小,以及脏页的数量。脏页是指有数据写入,但尚未同步到底层文件的页。因此,脏页的多少只对需要写回的映射有意义,即共享的文件支持型映射(4象限)。译者注:私有的文件支持型映射不需要写回,上一篇有讲。
pmap的数据源,可以在两个易读的文件中找到:/proc/[pid]/maps 和/proc/[pid]/smaps 。第一个文件只是一个简单的映射列表,第二个文件更详细的列举了每一个映射段。 smaps从Linux 2.6.14开始有效,目前流行的linux发布中都有它。
pmap的使用很简单:
pmap [pid] :显示/proc/[pid]/maps的内容,但删除了inode和device列。
pmap -x [pid]在输出中加入了来自/proc/[pid]/smaps ( RSS和Dirty )的信息。
自pmap 3.3.4开始,可以通过 -X和-XX来显示更多的数据,但这因Linux版本而异(在最近的内核版本中似乎有点问题)。
基本内容
pmap借鉴了Solaris上相似的命令,并且输出也大同小异。下面是共享匿名测试内存测试小程序运行时的pmap输出,以及/proc/[pid]/maps的内容:
在输出中有几个有趣的地方。首先, pmap选择输出的是映射的大小,而不是地址范围,并在结束时会统计这些大小的总和。这个总和是 top的VIRT大小:虚拟地址空间中所有申请内存的地址范围总和。
每个映射区间与一组模式相关联:
r :如果设置,表明映射区可读
w :如果设置,表明映射区可写
x :如果设置,表明映射区包含的是可执行代码
s :如果设置,表明映射区是共享的(我们前面分类的右边列)。你可以观察到pmap的输出只有s标记,而内核提供两种不同的标记:共享内存(s标记)和私有内存( p标记 )。
R :如果设置,映射区没有申请到交换空间( mmap的MAP_NORESERVE 标志),这意味着访问那块内存时如果它仍没被映射到物理内存则会导致段错误,并且系统没有可用的物理内存了。
前三个标志可以使用系统调用操纵,也可以在mmap调用时直接设置。
最后一列表明数据源。在我们的例子中,可以看到pmap不会显示内核特定实现的细节。它显示的内存有三种类型: anon , stack和文件支持型(文件的路径,如果映射文件已去除链接会有删除标记)。除了这些类别外,内核还有vdso , vsyscall 和heap 。遗憾的是pmap不包括heap,而它对程序员是很重要的(这可能是为了兼容Solaris)。
关于最后一列,我们也看到可执行文件和共享库是私有映射(在前面文章中已经提到过),并且同一个文件的不同部分会有不同的映射(部分甚至不止一次映射)。这是因为可执行文件包含不同的段:text,data,rodata,bss……每个段有不同的意义,并且进行不同的映射。在下一篇文章中我们会解释这些段的意义。
最后尤其需要注意的是,我们可以观察到共享的匿名内存实际上是通过对/dev/zero文件的无链接拷贝做共享映射来实现的。
扩展的内容
pmap -x的输出包含两个新增列:
第一个是RSS ,它告诉我们映射区中常驻内存的大小,并且通过统计最终提供了进程消耗的总内存大小。正如我们看到的,一些映射区中只有部分映射到了物理内存。最大的一个(我们手动mmap的那个)是完全分配的,因为我们修改了每一个页。
第二个新增列是Dirty,它包括由源文件所分配的页数。对于共享的文件支持型映射,如果内核须要释放一些RAM空间或者有太多的脏页,脏页可以写回底层的文件。在这种情况下,在页面被标记为clean 。对于剩下的象限,因为后端要么是匿名的(所以没有基于磁盘的后端)或私有的(页面的修改对其他进程无效),卸载脏页需要将它们写入交换文件 。
这仅是内核实际公开的一小部分。smaps文件中还有更多的信息(这使得它冗长而不可读),你可以在下面看到这些信息:
在bug报告中添加pmap的输出往往是个好主意。
还有吗?
正如你所看到的,理解top和其它工具的输出要求你了解正在运行的操作系统的相关知识。尽管top在各个系统上可用,但在不同版本的系统中有所不同。例如,在OS X上,你找不到RES , DATA , SHR 等列,取而代之的是RPRVT , RSHRD , RSIZE, VPRVT VSIZE (注意这些字段并不比Linux上的好理解)。如果你想对Linux的内存管理有更深的了解,你可以阅读 ,或阅读《 》。
这篇文章相当长的,这里做一个简短的总结:
top的man手册有问题。
top的RES告诉你进程实际所使用的物理内存大小。
top VIRT告诉你进程实际申请的虚拟内存大小。
top的DATA告诉你进程申请的虚拟私有匿名内存大小。该内存可能会也可能不会被映射到物理内存。它对应的是用于存储进程中特定数据(而不是共享)的内存大小。
top的SHR告诉你文件支持型(包括共享的匿名内存)的常驻内存大小。它显示了可能被其它进程共享的常驻内存大小。
top 的SWAP列只有在top (3.3.0)和Linux(2.6.34)的最新版本中是可信的,在所有其他情况下是没有意义的。
如果你想详细了解进程的内存使用情况,使用pmap。
如果你喜欢使用 ,请注意它的内容与top完全相同。它的man手册也有错误,至少有些地方不太清晰。
下一章:内存分配
在这两篇文章中,我们已经涵盖了开发人员和系统管理员感兴趣的部分(希望如此)。在文章中,我们将开始深入探索开发人员应该如何管理内存。
1.您可以尝试运行相同的程序不要循环(或用更短的循环),来观察内核是如何延迟物理页的加载。
2.但是我们在后面将会看到它只是加载可执行文件的数据段。
3.处于clean状态的私有文件支持页也可以被卸载,因此,下一次他们被加载时内容可能会改变,这取决于是否对文件进行了写操作。这是man手册中mmap的说明:调用mmap()后,对文件的修改,映射区是否可见并没有规定。
来源声明:本文来自Intersec Tech Talk的博文《》,由IDF实验室童进翻译。
(全文完)