VIRT:virtual memory usage 虚拟内存
1、进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
2、假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
3、VIRT = SWAP + RES

RES:resident memory usage 常驻内存
1、进程当前使用的内存大小,但不包括swap out
2、包含其他进程的共享
3、如果申请100m的内存,实际使用10m,它只增长10m,与VIRT相反
4、关于库占用内存的情况,它只统计加载的库文件所占内存大小
5、RES = CODE + DATA

SHR:shared memory 共享内存
1、除了自身进程的共享内存,也包括其他进程的共享内存
2、虽然进程只使用了几个共享库的函数,但它包含了整个共享库的大小
3、计算某个进程所占的物理内存大小公式:RES – SHR
4、swap out后,它将会降下来

DATA
1、数据占用的内存。如果top没有显示,按f键可以显示出来。
2、真正的该程序要求的数据空间,是真正在运行中要使用的。

VIRT RES SHR的准确含义

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

三个内存指标,VRIT,RES,SHR准确含义是什么?

那我们就去看下源码吧,这就是开源软件的最大的好处。

首先这三个数据的源头,肯定是内核,进程的相关数据结构肯定是由内核维护。那么top作为一个用户空间的程序,要想获取内核空间的数据,就需要通过系统接口(API)获取。而proc文件系统是Linux内核空间和用户空间交换数据的一个途径,而且是非常重要的一种途径,这点和windows更倾向于基于函数调用的形式不同。

当你调用系统函数read读取一个普通文件时,内核执行对应文件系统的代码从磁盘传送文件内容给你。

当你调用系统函数read读取一个 proc文件时,内核执行对应的proc文件系统的代码从内核的数据结构中传送相关内容给你。proc文件和磁盘没有关系。只是系统接口而已。

而一个进程的相关信息,Linux全部通过/proc/<pid>/内的文件告诉了我们。

如下,你可以使用普通的文件读写工具,比如cat获取进程的各种信息。这比函数调用的方式灵活多了、丰富多了。

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

回到我们的问题,top命令显示的进程信息,肯定也是通过proc获取的,因为除此之外没有其他途径,没有系统函数可以做这个事情,top也不可能越过用户层直取内核获取数据。

带着以上信息,很快就可以从top的源码中找到关键代码:

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 
 

啊哈,statm文件:

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

根据sscanf的顺序,第一个值是VIRT,第二个值是RES,第三个值是SHR!

等等,好像数值对不上,top显示的SHR是344k,而statm给出的是86!

再来看一行关键代码:

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 
 

statm显示的是页数,top显示的是KB。X86下,一页是4KB,86 * 4 = 344。这就对了!

于是乎,我们找到了最关键的入口,接下来按图索骥,看看内核是怎么产生statm文件内容就可以了。~~

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

proc_pid_statm函数负责产生statm文件内容,当你使用cat命令打印statm文件时,内核中的这个函数会执行。

proc_pid_statm获取进程的mm_struct数据结构,而这个数据结构就是进程的内存描述符,通过它可以获取进程内存使用、映射的全部信息。

     进一步考察task_statm函数,可以看到:

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

第一个值(VIRT)就是mm->total_vm,即进程虚存的总大小,这个比较清晰,只要进程申请了内存,无论是malloc还是堆栈还是全局,都会计入这个值;

第二个值(RES)是mm->file_rss+mm->anon_rss;

第三个值(SHR)是mm->file_rss。

 RES要和SHR结合者看,内核把物理内存分为了两部分,一部分是映射至文件的,一部分是没有映射至文件的即匿名内存,完全和共不共享没有关系!

但file_rss为什么叫做shared呢?应该是一种指示性表述,表示这部分内存可能是共享的。但并不代表真正共享了。那么到底哪些计入file_rss?通过查阅相关代码,发现(可能有遗漏):

程序的代码段。

动态库的代码段。

通过mmap做的文件映射。

通过mmap做的匿名映射,但指明了MAP_SHARED属性。

通过shmget申请的共享内存。

 即进程通过以上方式占用的物理内存,计入file_rss,也就是top的SHR字段。我们看到一般这些内存都是以共享方式存在。但如果某个动态库只一个进程在使用,它的代码段就没有被共享着。

反过来再来看anon_rss统计的内容,是否就一定是独占的?也不是,比如新fork之后的子进程,由于copy on write机制,在页面被修改之前,和父进程共享。这部分值并不体现在top命令的SHR字段内。

 综上所述top命令显示的SHR字段,并不是准确描述了进程与其他进程共享使用的内存数量,是存在误差的。 

那么如何获取进程准确的共享内存数量?

获取进程准确的共享内存数量

我们注意到在描述进程信息的proc/<pid>内,有一个smaps文件,里面展示了所有内存段的信息,其中有Shared_Clean Shared_Dirty Private_Clean Private_Dirty:几个字段

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 
 

找到相关代码,可以看到,一个页面如果映射数>=2计入Shared_* ; 如果=1计入Private_*。(脏页计入*_Dirty,否则计入*_Clean)

剖析top命令显示的VIRT RES SHR值 - yalung - Y A L U N G
 

     统计smaps文件内所有段的Shared_*值的总和就是进程准确的共享内存数量!

     统计smaps文件内所有段的Private_*值的总和就是进程准确的独占内存数量!

总结

通过以上分析,我们可以得到如下结论:

top命令通过解析/proc/<pid>/statm统计VIRT和RES和SHR字段值。

VIRT是申请的虚拟内存总量。

RES是进程使用的物理内存总和。

SHR是RES中映射至文件的物理内存总和。包括:

程序的代码段。

动态库的代码段。

通过mmap做的文件映射。

通过mmap做的匿名映射,但指明了MAP_SHARED属性。

通过shmget申请的共享内存。

/proc/<pid>/smaps内Shared_*统计的是RES中映射数量>=2的物理内存。

/proc/<pid>/smaps内Private_*统计的是RES中映射数量=1的物理内存。


top 运行中可以通过 top 的内部命令对进程的显示方式进行控制。内部命令如下:
s – 改变画面更新频率
l – 关闭或开启第一部分第一行 top 信息的表示
t – 关闭或开启第一部分第二行 Tasks 和第三行 Cpus 信息的表示
m – 关闭或开启第一部分第四行 Mem 和 第五行 Swap 信息的表示
N – 以 PID 的大小的顺序排列表示进程列表
P – 以 CPU 占用率大小的顺序排列进程列表
M – 以内存占用率大小的顺序排列进程列表
h – 显示帮助
n – 设置在进程列表所显示进程的数量
q – 退出 top
s – 改变画面更新周期

序号 列名 含义
a PID 进程id
b PPID 父进程id
c RUSER Real user name
d UID 进程所有者的用户id
e USER 进程所有者的用户名
f GROUP 进程所有者的组名
g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ?
h PR 优先级
i NI nice值。负值表示高优先级,正值表示低优先级
j P 最后使用的CPU,仅在多CPU环境下有意义
k %CPU 上次更新到现在的CPU时间占用百分比
l TIME 进程使用的CPU时间总计,单位秒
m TIME+ 进程使用的CPU时间总计,单位1/100秒
n %MEM 进程使用的物理内存百分比
o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。
q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
r CODE 可执行代码占用的物理内存大小,单位kb
s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
t SHR 共享内存大小,单位kb
u nFLT 页面错误次数
v nDRT 最后一次写入到现在,被修改过的页面数。
w S 进程状态。(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
x COMMAND 命令名/命令行
y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名
z Flags 任务标志,参考 sched.h

默认情况下仅显示比较重要的 PID、USER、PR、NI、VIRT、RES、SHR、S、%CPU、%MEM、TIME+、COMMAND 列。可以通过下面的快捷键来更改显示内容。

通过 f 键可以选择显示的内容。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,最后按回车键确定。
按 o 键可以改变列的显示顺序。按小写的 a-z 可以将相应的列向右移动,而大写的 A-Z 可以将相应的列向左移动。最后按回车键确定。
按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的 R 键可以将当前的排序倒转。

测试如下:

一)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char * p = new char [1024*1024*512];  
  6.     getchar();  
  7.     return 0;  
  8. }  

top结果如下:

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND

401 huyiyang  17   0  523m  916  792 S  0.0  0.0   0:00.00 ./main


VIRT包含了new出来的512MB空间,但是RES不包含该空间。即刚new出来的空间,如果没有使用,会放入SWAP中,并不在内容中真实的分配物理内存。

二)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char * p = new char [1024*1024*512];  
  6.     memset(p, 0, 1024*1024*512);  
  7.     getchar();  
  8.     return 0;  
  9. }  

top结果如下:

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                              

32604 test 17   0  523m 512m  792 S  0.0 26.2   0:00.33 ./main


VIRT包含new出来的512MB空间,RES包含目前使用的memset的512M空间。即new出来的空间被使用后,会真实分配物理内存。

三)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char * p = new char [1024*1024*512];  
  6.     memset(p + 1024*1024*128, 0, 1024*1024*128);  
  7.     getchar();  
  8.     return 0;  
  9. }  

top结果如下:
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 
456 test 17   0  523m 128m  792 S  0.0  6.6   0:00.07 ./main


VIRT包含new出来的512MB空间,RES包含目前使用的memset的128M空间。即new出来的空间,如果只使用部分,则只分配部分物理内存。

四)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char p[1024*1024*10];  
  6.     getchar();  
  7.     return 0;  
  8. }  

top结果如下:
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND

544 test 17   0 21568  884  760 S  0.0  0.0   0:00.00 ./main


没有使用的栈空间,VIRT会包含(没有使用的栈空间会在SWAP中)。

五)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char p[1024*1024*10];  
  6.     memset(p, 0, 1024*1024*10);  
  7.     getchar();  
  8.     return 0;  
  9. }  

top结果如下:

PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND

  561 test 17   0 21568  10m  760 S  0.0  0.6   0:00.00 ./main

已经使用的栈空间,VIRT和RES都会包含。

六)

[cpp]  view plain  copy
  1. #include <iostream>  
  2.   
  3. int main()  
  4. {  
  5.     char ** pp = new char * [1024];  
  6.     for(int i=0;i<1024;i++)  
  7.     {  
  8.         pp[i] = new char [1024*1024*512];  
  9.         memset(pp[i], 0, 1024*1024*512);  
  10.         printf("p%d\n", i);  
  11.         getchar();  
  12.     }  
  13.     return 0;  
  14. }  

第一个循环时:

  PID USER       PR  NI  VIRT    RES   SHR S %CPU %MEM    TIME+  SWAP COMMAND

 3627 test 18   0  523m 512m  836   S       0.0        26.2    0:00.34  10m ./main

第二个循环时:

  PID USER       PR  NI  VIRT    RES   SHR S %CPU %MEM    TIME+  SWAP COMMAND

 3627 huyiyang  18   0 1035m 1.0g  836    S     0.0      52.4      0:00.69  10m     ./main

第三个循环时:

PID   USER      PR  NI  VIRT   RES  SHR S %CPU %MEM    TIME+  SWAP COMMAND                                                                                                         
3627 test 18   0 1547m 1.5g    836 S     0.0       78.5   0:01.03    10m     ./main

在我的服务器上,当执行到第四个循环时,并且有其他程序占用较大内存的情况下,top结果如下:

  PID USER       PR  NI  VIRT    RES   SHR S %CPU %MEM    TIME+  SWAP COMMAND 

 3427 test 16   0 2059m 711m  836 S  0.0        36.4       0:01.45 1.3g     ./main

出现了swap out的情况。