[翻译]内存 - 第二部分:理解进程内存

原文地址:https://techtalk.intersec.com/2013/07/memory-part-2-understanding-process-memory/


#从虚拟内存到物理内存

在前一篇文章,我们介绍了一个方法来划分进程用到的内存。我们使用了两个纬度得到四个类别:私有/共享,匿名/基于文件。我们引入了共享机制的复杂度和所有内存都由内核分配回收的事实。

所有我们谈到的都是虚拟地址。所有关于内存地址的分配,内核并不总是立即把分配的地址映射到物理内存。大部分时候,内核总是延迟到对该地址的第一次访问(某些情况下是第一次写)才分配实际的物理内存。并且这个分配的粒度是以页(4KB)为单位。更进一步,一些页在分配后可能被交换出去,这意味着写到磁盘,从而可以让其他的页放入物理内存。

因此,想知道进程实际使用的物理内存(进程的resident内存)大小非常困难。但是内核作为系统的一个独立模块能够知道这个数据(这实际上也是它的其中一个工作)。幸运的是,内核提供了一些接口来让你获得系统和特定进程的一些统计数据。本文会深入了解Linux系统提供的工具,用于分享进程的内存模式。

在Linux上,那些统计数据通过/proc文件系统暴露出来,特别是/proc/[pid]下面的内容。这些目录(每个进程一个)包含一些伪文件,他们是直接获取内核信息的API。关于/proc目录的内容,更详细的信息可以查看proc(5)手册页(每个Linux版本的内容都会有一些改变)。

如procps(ps, top, pmap ...)这样的工具调用那些API并提供了人易于阅读的信息。这些工具把从内核取到的信息做少量的修改,或者完全不修改并输出。因此他们是一个很好的入口点来理解内核是如何对内存进行分类的。在这篇文章我们将分析top和pmap命令跟内存相关的输出。

top:进程统计

top是一个广为人知(并使用)的用于系统监控的工具。它在每一行用可变的列显示一个进程的相关信息,可以是关于CPU的,关于内存的或者其他更加通用的信息。

当top运行时,你可以按G3切换到内存视图。在这个试图,你会看到其中包含这些列:%MEMVIRTSWAPRESCODEDATASHR。除了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)。

显而易见的列

正如你猜想的那样,有些列很容易理解。VIRT是进程到目前位置分配的总虚拟地址空间大小。CODE是当前执行的二进制文件可执行代码的大小。RES是实际占用的内存大小,也就是内核认为分配给进程的物理内存的总数。因此%MEM是由RES计算得出的。

实际内存的大小是内核把两个计数器相加所得。第一个包含匿名物理内存页(MM_ANONPAGES)的数量。另一个是基于文件的内存页(MM_FILEPAGES)数量。一些页可能被认为同时被多个进程占用,因此RES的和可能比实际使用的物理内存大,甚至可能比系统可用的物理内存总量还大。

共享内存

SHR是进程实际使用的共享物理内存总数。如果你还记得我们上一篇文章对内存的分类,你可以假定它包含所有右边一列(注,上一篇2X2表格的右边一列中的两项)实际占用的内存。但是我们已经讨论过,有些私有内存也会被共享。因此,为了更好地理解这一列的实际含义,我们要更加深入的了解一下内核。

SHR列是用/proc/[pid]/statmshared字段的值填充得到的。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;
}



top显示SHR和RES都是50M。

这是由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_vmshared_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



注意top的man手册再一次出错。它指出DATA是除了进程可执行代码外实际分配的物理内存的总和,也称为“数据实际内存占用”大小或DRS。但是我们刚刚已经看到,DATA跟实际占用内存毫无联系。


SWAP

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]/statusVmSwap的值。它由内核维护,是一个计数器,每次在一个页被交换出去时增加,换进时减少。因此它是准确的,可以提供给你一个重要信息:基本上,如果这个值非0,说明你的系统有一些内存的压力,你的进程使用的内存不能在物理内存中全部放下。

man手册描述SWAP为任务的地址空间中不占用物理内存的部分,这是top3.3.0之前的实现,但是没有提到实际被交换出去的内存总数。在更早版本的top手册中正确解释了输出了什么,但是SWAP的名称并不合适。

pmap:详细映射信息

pmap是另外一种工具。它比top更加深入的显示了进程的每一个内存映射信息。在这个视图里,一个映射是一个包含连续页的范围,它们有相同的后端(匿名或文件)和相同的访问模式。

对每一个映射,pmap显示前面列出的选项和映射的大小,实际占用页的总数和脏页的总数。脏页是指已经被改写过但是还没有同步到对应文件的页。因此,脏页的数量只对需要回写的映射有意义,即共享的基于文件的映射(类别4)。

pmap的数据源是两个人可阅读的文件:/proc/[pid]/maps/proc/[pid]/smaps。第一个文件只是简单列出了映射,第二个文件对每个映射用一个单独的段列出了更详细的信息。Linux2.6.14开始支持smaps,这个版本已经很老,因此目前流行的发行版都支持。

pmap的用法很简单:


  • pmap [pid]:展示/proc/[pid]/maps文件的内容,但是移除inode和device列。
  • pmap -x [pid]:内容更加丰富的输出,加上了/proc/[pid]/smaps文件的内容(RSS和Dirty)。
  • 从pmap3.3.4开始,有-x和-xx选项支持显示更多的数据,但是需要特定的Linux版本(并且最新的内核版本似乎有一些bug)。

基本内容

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]



输出中有一些有意思的东西。首先,pmap选择提供映射的大小而不是地址范围,并且在最后加了总和。这个总和是top中的VIRT:虚拟地址空间中所有分配的地址范围的总和。


每一个映射都有相关的一个模式的集合:


  • r:如果设置,映射是可读的
  • w:如果设置,映射是可写的
  • x:如果设置,映射包含可执行代码
  • s:如果设置,映射是共享的(我们前面的分类表格的右边一列)。你可以注意到,pmap只有s标志,但是内核暴露出两个两个不同的标志,共享的(s)和私有的(p)
  • R:如果设置,映射没有分配交换空间(mmap时设置MAP_NORESERVE标志),这意味着我们访问该内存,但是该内存没有被映射到物理内存,且当时没有物理内存时,我们会得到段错误。

前面三个标志可以通过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



第一个是RSS,它告诉我们哪些映射是实际占用内存的(最后提供一个进程所有实际占用内存的总和)。我们可以看到映射只会部分的映射到物理内存。最大的那个(我们调用mmap分配的)是完全分配的,因为我们访问了每一页。

第二个新的列是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



把pmap的输出放到一个bug报告里通常是一个好主意。

更多?

你可以看到,理解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]。

因为本篇有点长,这里做个总结:


  • 不能信任top的man手册
  • top的RES告诉你进程实际使用的物理内存
  • top的VIRT告诉你进程实际分配的虚拟内存
  • top的DATA告诉你进程进程分配的私有匿名内存总数。这些内存可能被映射到物理内存,也可能没有。它由进程想要存放的数据的数量决定。
  • top的SHR告诉你基于文件的内存实际占用的大小(包含匿名共享内存)。它表示实际占用的,可以被其他进程使用的内存。
  • top的SWAP列仅仅在3.3.0版本和Linux2.6.34版本之后可以被信任。在其他情况下毫无意义。
  • 如果你想知道一个进程使用内存的细节,使用pmap。

如果你喜欢用htop,它的内容和top是完全一样的。它的man手册也是错误的,或者至少是不清楚的。


转载于:https://my.oschina.net/tz8101/blog/629739

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值