Chinaunix首页 | 论坛 | 博客
  • 博客访问: 759418
  • 博文数量: 247
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 501
  • 用 户 组: 普通用户
  • 注册时间: 2013-07-12 21:53
个人简介

系统未建立

文章分类

全部博文(247)

文章存档

2021年(1)

2020年(3)

2019年(5)

2018年(3)

2017年(44)

2016年(75)

2015年(52)

2014年(63)

2013年(1)

我的朋友

分类: LINUX

2016-11-02 10:36:49

作者: 黄永兵/译 出处:51CTO.com 
阅读提示:本文是为那些经常疑惑的人准备的,“为什么一个简单的KDE文本编辑器要占用25M内存?”导致大多数人认为许多应用程序,特别是KDE或GNOME程序都象ps报告一样臃肿...【51CTO.com独家译文】
本文是为那些经常疑惑的人准备的,“为什么一个简单的KDE文本编辑器要占用25M内存?”导致大多数人认为许多Linux应用程序,特别是KDE或GNOME程序都象ps报告一样臃肿,虽然这可能是也可能不是真的,依赖于具体的程序,它通常不是真的,一些程序比它们看起来消耗更多的内存。ps工具能为一个进程输出许多块有关的信息,象进程ID,当前运行状态,资源利用情况等。其中可能输出VSZ(代表虚拟设置大小)和RSS(驻留设置大小),它们经常被世界各地的计算机爱好者用来查看进程占用了多少内存。例如:下面是在我电脑上用ps aux命令为KEit的输出:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDdbunker 3468 0.0 2.7 25400 14452 ? S 20:19 0:00 kdeinit:kedit 按照ps的输出,KEdit占用了大约25M的虚拟大小内存空间,大约14M驻留大小空间(上面报告中的两个数字都用k为单位),看起来大部分人都喜欢随意选择其中一个数字来表示某个进程的真实内存占用情况。我现在暂时先不解释VSZ和RSS之间的不同之处。不用说,前面那种认识是错误的!
想要知道为什么,必须先学习Linux是如何在程序中控制共享库的。在Linux上的大部分主要程序使用共享库有助于确定功能,例如:一个KDE文件编辑程序将使用几个KDE共享库(为了允许与其他的KDE组件进行交互),几个X库(为了允许它显示、拷贝和粘贴图像)和几个常用系统库(为了允许它执行基本的操作)。大部分这些库,特别是象libc这样常用的库,是被许多Linux程序使用的,正是由于有这些共享,Linux可以使用一个巨大的诀窍:它将只载入单个共享库的拷贝到内存中,使用这一个拷贝就可以供每个引用它的程序使用。许多工具不再关心这个非常通用的技巧,这可能是个好现象也可能是个坏现象;它们只是简单地报告某个进程使用了多少内存,而不管是否是与其他进程共享了部分内存,两个程序使用一个很大的共享库,并且它的大小倾向于它们内存使用的总和,共享库被计入了双倍的大小,如果你不清楚这一点将使你产生误解。
不幸的是,关于进程内存使用的准确表示法不是那么容易获得,不仅需要你理解系统是如何真实地工作的,而且还需要解决你想处理的一些困难问题,一个共享库应该为那个使用它的进程内存使用进行计数吗?如果一个共享库被许多进程使用,在这些进程间它的内存使用是平均分布的吗?或者刚好可以忽略?没有一个确定的及快速的规则,依赖于你面对的位置你可能会有不同的答案。这下容易看出来为什么ps不会尽力尝试报告一个正确的内存使用总量,而是给出一个模糊的数字。看一个进程的内存映像足以说明,让我们来看一看那个庞大的Kedit进程的位置,要查看Kedit的内存象什么样子,我们将使用pmap程序(使用-d标志)
Address Kbytes Mode Offset Device Mapping
08048000 40 r-x– 0000000000000000 0fe:00000 kdeinit
08052000 4 rw— 0000000000009000 0fe:00000 kdeinit
08053000 1164 rw— 0000000008053000 000:00000 [ anon ]
40000000 84 r-x– 0000000000000000 0fe:00000 ld-2.3.5.so
40015000 8 rw— 0000000000014000 0fe:00000 ld-2.3.5.so
40017000 4 rw— 0000000040017000 000:00000 [ anon ]
40018000 4 r-x– 0000000000000000 0fe:00000 kedit.so
40019000 4 rw— 0000000000000000 0fe:00000 kedit.so
40027000 252 r-x– 0000000000000000 0fe:00000 libkparts.so.2.1.0
40066000 20 rw— 000000000003e000 0fe:00000 libkparts.so.2.1.0
4006b000 3108 r-x– 0000000000000000 0fe:00000 libkio.so.4.2.0
40374000 116 rw— 0000000000309000 0fe:00000 libkio.so.4.2.0
40391000 8 rw— 0000000040391000 000:00000 [ anon ]
40393000 2644 r-x– 0000000000000000 0fe:00000 libkdeui.so.4.2.0
40628000 164 rw— 0000000000295000 0fe:00000 libkdeui.so.4.2.0
40651000 4 rw— 0000000040651000 000:00000 [ anon ]
40652000 100 r-x– 0000000000000000 0fe:00000 libkdesu.so.4.2.0
4066b000 4 rw— 0000000000019000 0fe:00000 libkdesu.so.4.2.0
4066c000 68 r-x– 0000000000000000 0fe:00000 libkwalletclient.so.1.0.0
4067d000 4 rw— 0000000000011000 0fe:00000 libkwalletclient.so.1.0.0
4067e000 4 rw— 000000004067e000 000:00000 [ anon ]
4067f000 2148 r-x– 0000000000000000 0fe:00000 libkdecore.so.4.2.0
40898000 64 rw— 0000000000219000 0fe:00000 libkdecore.so.4.2.0
408a8000 8 rw— 00000000408a8000 000:00000 [ anon ]
…. (trimmed) …mapped: 25404K writeable/private: 2432K shared: 0K
我剪掉了许多输出内容,剩下的与展示出来的类似,即使没有完整的输出,我们也可以看到一些非常有趣的内容,一个重要的内容就是注意到每一个共享库都列出了两次,一次为它的代码段一次为它的数据段,代码段具有“r-x-”样式,而数据段具有“rw--”样式,我们关心的只有字节数、样式和映像栏,剩下的对于讨论都是不重要的。如果你仔细检查输出内容,你会发现最大字节数的行通常是包含共享库(以lib开头的行就是共享库)的代码段行,如果你找出了在进程之间所有的共享部分,它们以“writeable/private” 结束,显示在输出的底端,这可以理解为进程的消耗增量,因此,运行Kedit(假设所有共享库都已经被载入了)实例大约要占用2M,这和ps报告的14或25M完全不是一回事。这意味着什么?这个故事的寓意是Linux进程内存使用是一个复杂的事情,你不能仅通过运行ps来了解,当你处理一个创建了大量子进程的程序时特别真实,如Apache,ps可能报告每个Apache进程使用10M内存,实际上每个Apache进程只消耗了1M内存,在调整Apache的MaxClients参数(它决定了你的服务器能同时处理的请求数量,)设置时这个信息变得非常重要。同时,它也适应于桌面软件,如果你运行了KDE,但是几乎全部使用Gnome应用程序,那么你将为多余的(但是不同的)共享库付出巨大的代价,因此请尽量保持要么全部运行KDE应用程序要么全部运行Gnome应用程序,这样Linux就可以使用更多的内存来做其他事情(如文件缓存,它可以极大地提高文件的访问速度)。原文出处:


想必在linux上写过程序的同学都有分析进程占用多少内存的经历,或者被问到这样的问题——你的程序在运行时占用了多少内存(物理内存)?通常我们可以通过top命令查看进程占用了多少内存。这里我们可以看到VIRT、RES和SHR三个重要的指标,他们分别代表什么意思呢?这是本文需要跟大家一起探讨的问题。当然如果更加深入一点,你可能会问进程所占用的那些物理内存都用在了哪些地方?这时候top命令可能不能给到你你所想要的答案了,不过我们可以分析proc文件系统提供的smaps文件,这个文件详尽地列出了当前进程所占用物理内存的使用情况。

这篇blog总共分为三个部分。第一部分简要阐述虚拟内存和驻留内存这两个重要的概念;第二部分解释top命令中VIRT、RES以及SHR三个参数的实际参考意义;最后一部分向大家介绍一下smaps文件的格式,通过分析smaps文件我们可以详细了解进程物理内存的使用情况,比如mmap文件占用了多少空间、动态内存开辟消耗了多少空间、函数调用栈消耗了多少空间等等。

关于内存的两个概念

要理解top命令关于内存使用情况的输出,我们必须首先搞清楚虚拟内存(Virtual Memory)和驻留内存(Resident Memory)两个概念。

【虚拟内存】

首先需要强调的是虚拟内存不同于物理内存,虽然两者都包含内存字眼但是它们属于两个不同层面的概念。进程占用虚拟内存空间大并非意味着程序的物理内存也一定占用很大。虚拟内存是内核为了对进程地址空间进行管理(process address space management)而精心设计的一个逻辑意义上的内存空间概念。我们程序中的指针其实都是这个虚拟内存空间中的地址。比如我们在写完一段C++程序之后都需要采用g++进行编译,这时候编译器采用的地址其实就是虚拟内存空间的地址。因为这时候程序还没有运行,何谈物理内存空间地址?凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中。既然说虚拟内存是一个逻辑意义上(假象的)的内存空间,为了能够让程序在物理机器上运行,那么必须有一套机制可以让这些假象的虚拟内存空间映射到物理内存空间(实实在在的RAM内存条上的空间)。这其实就是操作系统中页映射表(page table)所做的事情了。内核会为系统中每一个进程维护一份相互独立的页映射表。。页映射表的基本原理是将程序运行过程中需要访问的一段虚拟内存空间通过页映射表映射到一段物理内存空间上,这样CPU访问对应虚拟内存地址的时候就可以通过这种查找页映射表的机制访问物理内存上的某个对应的地址。“页(page)”是虚拟内存空间向物理内存空间映射的基本单元。

      下图1演示了虚拟内存空间和物理内存空间的相互关系,它们通过Page Table关联起来。其中虚拟内存空间中着色的部分分别被映射到物理内存空间对应相同着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。这么做也是本着“按需映射”的指导思想,因为虚拟内存空间很大,可能其中很多部分在一次程序运行过程中根本不需要访问,所以也就没有必要将虚拟内存空间中的这些部分映射到物理内存空间上。
到这里为止已经基本阐述了什么是虚拟内存了。总结一下就是,虚拟内存是一个假象的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间中。虚拟内存空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。
t1

驻留内存】

驻留内存,顾名思义是指那些被映射到进程虚拟内存空间的物理内存。上图1中,在系统物理内存空间中被着色的部分都是驻留内存。比如,A1、A2、A3和A4是进程A的驻留内存;B1、B2和B3是进程B的驻留内存。进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虚拟内存大并不意味着占用的物理内存大。

关于虚拟内存和驻留内存这两个概念我们说到这里。下面一部分我们来看看top命令中VIRT、RES和SHR分别代表什么意思。

top命令中VIRT、RES和SHR的含义
      搞清楚了虚拟内存的概念之后解释VIRT的含义就很简单了。VIRT表示的是进程虚拟内存空间大小。对应到图1中的进程A来说就是A1、A2、A3、A4以及灰色部分所有空间的总和。也就是说VIRT包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分总和。
RES的含义是指进程虚拟内存空间中已经映射到物理内存空间的那部分的大小。对应到图1中的进程A来说就是A1、A2、A3以及A4几个部分空间的总和。所以说,看进程在运行过程中占用了多少内存应该看RES的值而不是VIRT的值。
最后来看看SHR所表示的含义。SHR是share(共享)的缩写,它表示的是进程占用的共享内存大小。在上图1中我们看到进程A虚拟内存空间中的A4和进程B虚拟内存空间中的B3都映射到了物理内存空间的A4/B3部分。咋一看很奇怪。为什么会出现这样的情况呢?其实我们写的程序会依赖于很多外部的动态库(.so),比如libc.so、libld.so等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映射到对应进程的虚拟内存空间中。多个进展之间通过共享内存的方式相互通信也会出现这样的情况。这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,所以我们将他们称为共享内存,用SHR来表示。某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可。

进程的smaps文件

查看命令是:cat /proc/进程的pid/smaps

通过top命令我们已经能看出进程的虚拟空间大小(VIRT)、占用的物理内存(RES)以及和其他进程共享的内存(SHR)。但是仅此而已,如果我想知道如下问题:

  1. 进程的虚拟内存空间的分布情况,比如heap占用了多少空间、文件映射(mmap)占用了多少空间、stack占用了多少空间?
  2.  进程是否有被交换到swap空间的内存,如果有,被交换出去的大小?
  3. mmap方式打开的数据文件有多少页在内存中是脏页(dirty page)没有被写回到磁盘的?
  4. mmap方式打开的数据文件当前有多少页面已经在内存中,有多少页面还在磁盘中没有加载到page cahe中?
  5. 等等

以上这些问题都无法通过top命令给出答案,但是有时候这些问题正是我们在对程序进行性能瓶颈分析和优化时所需要回答的问题。所幸的是,世界上解决问题的方法总比问题本身要多得多。linux通过proc文件系统为每个进程都提供了一个smaps文件,通过分析该文件我们就可以一一回答以上提出的问题。

在smaps文件中,每一条记录(如下图2所示)表示进程虚拟内存空间中一块连续的区域。其中第一行从左到右依次表示地址范围、权限标识、映射文件偏移、设备号、inode、文件路径。详细解释可以参见。

接下来8个字段的含义分别如下:

  1. Size:表示该映射区域在虚拟内存空间中的大小。
  2. Rss:表示该映射区域当前在物理内存中占用了多少空间。
  3. Shared_Clean:和其他进程共享的未被改写的page的大小。
  4. Shared_Dirty: 和其他进程共享的被改写的page的大小。
  5. Private_Clean:未被改写的私有页面的大小。
  6. Swap:表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小。
  7. Pss:该虚拟内存区域平摊计算后使用的物理内存大小(有些内存会和其他进程共享,例如mmap进来的)。比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB,那么该进程分摊其中一半的内存,即Pss=500KB。
  8. t_2

图2. smaps文件示例

有了smap如此详细关于虚拟内存空间到物理内存空间的映射信息,相信大家已经能够通过分析该文件回答上面提出的4个问题。

最后希望所有读者能够通过阅读本文对进程的虚拟内存和物理内存有一个更加清晰认识,并能更加准确理解top命令关于内存的输出,最后可以通过smaps文件更进一步分析进程使用内存的情况





 http://yalung929.blog.163.com/blog/static/203898225201212981731971/


引 言: top命令作为Linux下最常用的性能分析工具之一,可以监控、收集进程的CPUIO、内存使用情况。比如我们可以通过top命令获得一个进程使用了多少虚拟内存(VIRT)、物理内存(RES)、共享内存(SHR)。

最近遇到一个咨询问题,某产品做性能分析需要获取进程占用物理内存的实际大小(不包括和其他进程共享的部分),看似很简单的问题,但经过研究分析后,发现背后有很多故事……

VIRT RES SHR的准确含义

 

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

三个内存指标,VRITRESSHR准确含义是什么?谁能告诉我们?MAN页?Linux专家?SUSE工程师?Linus?谁能说出最正确答案?没人!因为惟有源代码才是最正确的答案。

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

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

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

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

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

如下,你可以使用普通的文件读写工具,比如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显示的SHR344k,而statm给出的是86

再来看一行关键代码:

 

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

statm显示的是页数,top显示的是KBX86下,一页是4KB86 * 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,也就是topSHR字段。我们看到一般这些内存都是以共享方式存在。但如果某个动态库只一个进程在使用,它的代码段就没有被共享着。

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

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

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

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

我们注意到在描述进程信息的proc/内,有一个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//statm统计VIRTRESSHR字段值。

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

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

SHRRES映射至文件的物理内存总和。包括:

程序的代码段。

动态库的代码段。

通过mmap做的文件映射。

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

通过shmget申请的共享内存。

/proc//smapsShared_*统计的是RES映射数量>=2的物理内存。

/proc//smapsPrivate_*统计的是RES映射数量=1的物理内存。






在Linux下查看内存我们一般用free命令:
[root@scs-2 tmp]# free
             total       used       free     shared    buffers     cached
Mem:       3266180    3250004      16176          0     110652    2668236
-/+ buffers/cache:     471116    2795064
Swap:      2048276      80160    1968116

下面是对这些数值的解释:
total:总计物理内存的大小。
used:已使用多大。
free:可用有多少。
Shared:多个进程共享的内存总额。
Buffers/cached:磁盘缓存的大小。
第三行(-/+ buffers/cached):
used:已使用多大。
free:可用有多少。
第四行就不多解释了。
区别:第二行(mem)的used/free与第三行(-/+ buffers/cache) used/free的区别。 这两个的区别在于使用的角度来看,第一行是从OS的角度来看,因为对于OS,buffers/cached 都是属于被使用,所以他的可用内存是16176KB,已用内存是3250004KB,其中包括,内核(OS)使用+Application(X, ,etc)使用的+buffers+cached.
第三行所指的是从应用程序角度来看,对于应用程序来说,buffers/cached 是等于可用的,因为buffer/cached是为了提高文件读取的性能,当应用程序需在用到内存的时候,buffer/cached会很快地被回收。
所以从应用程序的角度来说,可用内存=系统free memory+buffers+cached。
如上例:
2795064=16176+110652+2668236

接下来解释什么时候内存会被交换,以及按什么方交换。 当可用内存少于额定值的时候,就会开会进行交换。
如何看额定值:
cat /proc/meminfo

[root@scs-2 tmp]# cat /proc/meminfo
MemTotal:      3266180 kB
MemFree:         17456 kB
Buffers:        111328 kB
Cached:        2664024 kB
SwapCached:          0 kB
Active:         467236 kB
Inactive:      2644928 kB
HighTotal:           0 kB
HighFree:            0 kB
LowTotal:      3266180 kB
LowFree:         17456 kB
SwapTotal:     2048276 kB
SwapFree:      1968116 kB
Dirty:               8 kB
Writeback:           0 kB
Mapped:         345360 kB
Slab:           112344 kB
Committed_AS:   535292 kB
PageTables:       2340 kB
VmallocTotal: 536870911 kB
VmallocUsed:    272696 kB
VmallocChunk: 536598175 kB
HugePages_Total:     0
HugePages_Free:      0
Hugepagesize:     2048 kB

用free -m查看的结果:
[root@scs-2 tmp]# free -m 
             total       used       free     shared    buffers     cached
Mem:          3189       3173         16          0        107       2605
-/+ buffers/cache:        460       2729
Swap:         2000         78       1921


查看/proc/kcore文件的大小(内存镜像):
[root@scs-2 tmp]# ll -h /proc/kcore 
-r-------- 1 root root 4.1G Jun 12 12:04 /proc/kcore

备注:

占用内存的测量

测量一个进程占用了多少内存,linux为我们提供了一个很方便的方法,/proc目录为我们提供了所有的信息,实际上top等工具也通过这里来获取相应的信息。

/proc/meminfo 机器的内存使用信息

/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。

/proc/pid/statm 进程所占用的内存

[root@localhost ~]# cat /proc/self/statm

654 57 44 0 0 334 0

输出解释

CPU 以及CPU0。。。的每行的每个参数意思(以第一行为例)为:

参数 解释 /proc//status

Size (pages) 任务虚拟地址空间的大小 VmSize/4

Resident(pages) 应用程序正在使用的物理内存的大小 VmRSS/4

Shared(pages) 共享页数 0

Trs(pages) 程序所拥有的可执行虚拟内存的大小 VmExe/4

Lrs(pages) 被映像到任务的虚拟内存空间的库的大小 VmLib/4

Drs(pages) 程序数据段和用户态的栈的大小 (VmData+ VmStk )4

dt(pages) 04

查看机器可用内存

/proc/28248/>free

total used free shared buffers cached

Mem: 1023788 926400 97388 0 134668 503688

-/+ buffers/cache: 288044 735744

Swap: 1959920 89608 1870312

我们通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在linux中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。

所以 空闲内存=free+buffers+cached=total-used


 

 

用/proc文件系统查看进程的内存使用情况

/proc目录Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统

/proc/vmstat 虚拟内存统计信息

/proc/vmcore 内核panic时的内存映像

/proc/diskstats 取得磁盘信息

/proc/schedstat kernel调度器的统计信息

/proc/zoneinfo 显示内存空间的统计信息,对分析虚拟内存行为很有用

以下是/proc目录中进程N的信息

/proc/N pid为N的进程信息

/proc/N/cmdline 进程启动命令

/proc/N/cwd 链接到进程当前工作目录

/proc/N/environ 进程环境变量列表

/proc/N/exe 链接到进程的执行命令文件

/proc/N/fd 包含进程相关的所有的文件描述符

/proc/N/maps 与进程相关的内存映射信息

/proc/N/mem 指代进程持有的内存,不可读

/proc/N/root 链接到进程的根目录

/proc/N/stat 进程的状态

/proc/N/statm 进程使用的内存的状态

/proc/N/status 进程状态信息,比stat/statm更具可读性

/proc/self 链接到当前正在运行的进程


ps命令的输出关于内存的情况不是很详细,尤其是进程所使用的内存中有很大一部分是共享库函数使用的,因此通过ps命令的输出看不到进程自己使用了多少内存。为了查看更详细的信息,可以借助于/proc文件系统。这个文件系统并存在于磁盘上,但是可以象操作其它普通文件一样操作它。它是Linux提供给用户查看进程相关信息的接口。在/proc下有2个文件和进程内存有关:/proc//status和/proc//smaps。

通过/proc//status可以查看进程的内存使用情况,包括虚拟内存大小(VmSize),物理内存大小(VmRSS),数据段大小(VmData),栈的大小(VmStk),代码段的大小(VmExe),共享库的代码段大小(VmLib)等等。

$ cat /proc/10069/status
Name:   a.out
State:  S (sleeping)
Tgid:   10069
Pid:    10069
PPid:   6793
TracerPid:      0
Uid:    1001    1001    1001    1001
Gid:    1001    1001    1001    1001
FDSize: 256
Groups: 1000 1001 
VmPeak:     1692 kB
VmSize:     1616 kB
VmLck:         0 kB
VmHWM:       304 kB
VmRSS:       304 kB
VmData:       28 kB
VmStk:        88 kB
VmExe:         4 kB
VmLib:      1464 kB
VmPTE:        20 kB
Threads:        1
SigQ:   0/16382
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Cpus_allowed:   f
Cpus_allowed_list:      0-3
Mems_allowed:   1
Mems_allowed_list:      0
voluntary_ctxt_switches:        1
nonvoluntary_ctxt_switches:     1注意,VmData,VmStk,VmExe和VmLib之和并不等于VmSize。这是因为共享库函数的数据段没有计算进去(VmData仅包含a.out程序的数据段,不包括共享库函数的数据段,也不包括通过mmap映射的区域。VmLib仅包括共享库的代码段,不包括共享库的数据段)。

通过/proc//smaps可以查看进程整个虚拟地址空间的映射情况,它的输出从低地址到高地址按顺序输出每一个映射区域的相关信息,如下所示:

$ cat /proc/10069/smaps
00110000-00263000 r-xp 00000000 08:07 128311     /lib/tls/i686/cmov/libc-2.11.1.so
Size:               1356 kB
Rss:                 148 kB
Pss:                   8 kB
Shared_Clean:        148 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:          148 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
......
......
bfd7f000-bfd94000 rw-p 00000000 00:00 0          [stack]
Size:                 88 kB
Rss:                   8 kB
Pss:                   8 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         8 kB
Referenced:            8 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB注意:rwxp中,p表示私有映射(采用Copy-On-Write技术)。 Size字段就是该区域的大小。




一提到内存管理,我们头脑中闪出的两个概念,就是虚拟内存,与物理内存。这两个概念主要来自于linux内核的支持。

Linux在内存管理上份为两级,一级是线性区,类似于00c73000-00c88000,对应于虚拟内存,它实际上不占用实际物理内存;一级是具体的物理页面,它对应我们机器上的物理内存。

这里要提到一个很重要的概念,内存的延迟分配。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。


点击(此处)折叠或打开

  1. char *p=malloc(2048)//这里只是分配了虚拟内存2048,并不占用实际内存。
  2. strcpy(p,”123”)//分配了物理页面,虽然只是使用了3个字节,但内存还是为它分配了2048字节的物理内存。
  3. free(p)//通过虚拟地址,找到其所对应的物理页面,释放物理页面,释放线性区。


我们知道用户的进程和内核是运行在不同的级别,进程与内核之间的通讯是通过系统调用来完成的。进程在申请和释放内存,主要通过brk,sbrk,mmap,unmmap这几个系统调用,传递的参数主要是对应的虚拟内存。


注意一点,在进程只能访问虚拟内存,它实际上是看不到内核物理内存的使用,这对于进程是完全透明的。

 

glibc内存管理器

那么我们每次调用malloc来分配一块内存,都进行相应的系统调用呢?

答案是否定的,这里我要引入一个新的概念,glibc的内存管理器。

我们知道malloc和free等函数都是包含在glibc库里面的库函数,我们试想一下,每做一次内存操作,都要调用系统调用的话,那么程序将多么的低效。

实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。

 

内存管理器面临的困难

我们在写程序的时候,每次申请的内存块大小不规律,而且存在频繁的申请和释放,这样不可避免的就会产生内存碎块。而内存碎块,直接会导致大块内存申请无法满足,从而更多的占用系统资源;如果进行碎块整理的话,又会增加cpu的负荷,很多都是互相矛盾的指标,这里我就不细说了。

我们在写程序时,涉及内存时,有两个概念heap和stack。传统的说法stack的内存地址是向下增长的,heap的内存地址是向上增长的。

函数malloc和free,主要是针对heap进行操作,由程序员自主控制内存的访问。

在这里heap的内存地址向上增长,这句话不完全正确。

glibc对于heap内存申请大于128k的内存申请,glibc采用mmap的方式向内核申请内存,这不能保证内存地址向上增长;小于128k的则采用brk,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。

这里我先讲大块内存的申请,也即对应于mmap系统调用。

对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放,这个过程中间不会产生内存碎块等问题。

针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次进行内存申请时,glibc会将堆顶向上增长来扩展内存空间,也就是我们所说的堆地址向上增长。在对这些小块内存进行操作时,便会产生内存碎块的问题。实际上brk和sbrk系统调用,就是调整heap顶地址指针。

 

那么heap堆的内存是什么时候释放呢?

当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。

下面我要讲一个内存空洞的问题:

一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?

很遗憾,不能。

这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?

这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。

提一个问题:


点击(此处)折叠或打开

  1. char *p=malloc(2);
  2. free(p)

为什么申请内存的时候,需要两个参数,一个是内存大小,一个是返回的指针;而释放内存的时候,却只要内存的指针呢?


这主要是和glibc的内存管理机制有关。glibc中,为每一块内存维护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的大小,然后是分配给进程的内存。


点击(此处)折叠或打开

  1. chunk
  2.  ------size
  3. p------------
  4.  content

在进程释放内存时,只要 指针-4 便可以找到该块内存的大小,从而释放掉。


注:glibc在做内存申请时,最少分配16个字节,以便能够维护chunk结构。

glibc提供的调试工具:

为了方便调试,glibc 为用户提供了 malloc 等等函数的钩子(hook),如 __malloc_hook

对应的是一个函数指针,


点击(此处)折叠或打开

  1. void*function(size_t
  2.  size, constvoid*caller)

其中 caller 是调用 malloc 返回值的接受者(一个指针的地址)。另外有 __malloc_initialize_hook函数指针,仅仅会调用一次(第一次分配动态内存时)。(malloc.h)


一些使用 malloc 的统计量(SVID 扩展)可以用 struct mallinfo 储存,可调用获得。

点击(此处)折叠或打开

  1. struct mallinfo mallinfo (void)

如何检测 memory leakage?glibc 提供了一个函数

void mtrace (void)及其反作用void muntrace (void)

这时会依赖于一个环境变量 MALLOC_TRACE 所指的文件,把一些信息记录在该文件中

用于侦测 memory leakage,其本质是安装了前面提到的 hook。一般将这些函数用

#ifdef DEBUGGING 包裹以便在非调试态下减少开销。产生的文件据说不建议自己去读,

而使用 mtrace 程序(perl 脚本来进行分析)。下面用一个简单的例子说明这个过程,这是

源程序:

点击(此处)折叠或打开

  1. #include
  2. #include
  3. #include
  4. intmain(intargc,
  5.  char *argv[] )
  6. {
  7.   int*p,
  8.  *q ;
  9.   #ifdef
  10.  DEBUGGING
  11.   mtrace(
  12.  ) ;
  13.   #endif
  14.   p
  15.  = malloc( sizeof( int)
  16.  ) ;
  17.   q
  18.  = malloc( sizeof( int)
  19.  ) ;
  20.   printf("p
  21.  = %p\nq = %p\n",
  22.  p, q ) ;
  23.   *p
  24.  = 1 ;
  25.   *q
  26.  = 2 ;
  27.   free(
  28.  p ) ;
  29.   return0
  30.  ;
  31. }

很简单的程序,其中 q 没有被释放。我们设置了环境变量后并且 touch 出该文件

执行结果如下:

点击(此处)折叠或打开

  1. p
  2.  = 0x98c0378q = 0x98c0388

该文件内容如下

点击(此处)折叠或打开

  1. =
  2.  Start
  3. @./test30:[0x8048446]
  4.  + 0x98c03780x4
  5. @./test30:[0x8048455]
  6.  + 0x98c03880x4
  7. @./test30:[0x804848f]
  8.  - 0x98c0378

到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。

 

代码占用的内存

数据部分占用内存,那么我们写的程序是不是也占用内存呢?

在linux中,程序的加载,涉及到两个工具,linker 和loader。Linker主要涉及动态链接库的使用,loader主要涉及软件的加载。

  1. exec执行一个程序
  2. elf为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;另一个段是数据段,它是可读写,不能执行。
  3. loader会启动,通过mmap系统调用,将代码端和数据段映射到内存中,其实也就是为其分配了虚拟内存,注意这时候,还不占用物理内存;只有程序执行到了相应的地方,内核才会为其分配物理内存。
  4.  loader会去查找该程序依赖的链接库,首先看该链接库是否被映射进内存中,如果没有使用mmap,将代码段与数据段映射到内存中,否则只是将其加入进程的地址空间。这样比如glibc等库的内存地址空间是完全一样。

因此一个2M的程序,执行时,并不意味着为其分配了2M的物理内存,这与其运行了的代码量,与其所依赖的动态链接库有关。

 

运行过程中链接动态链接库与编译过程中链接动态库的区别

我们调用动态链接库有两种方法:一种是编译的时候,指明所依赖的动态链接库,这样loader可以在程序启动的时候,来所有的动态链接映射到内存中;一种是在运行过程中,通过dlopen和dlfree的方式加载动态链接库,动态将动态链接库加载到内存中。

这两种方式,从编程角度来讲,第一种是最方便的,效率上影响也不大,在内存使用上有些差别。

第一种方式,一个库的代码,只要运行过一次,便会占用物理内存,之后即使再也不使用,也会占用物理内存,直到进程的终止。

第二中方式,库代码占用的内存,可以通过dlfree的方式,释放掉,返回给物理内存。

这个差别主要对于那些寿命很长,但又会偶尔调用各种库的进程有关。如果是这类进程,建议采用第二种方式调用动态链接库





包含了所有CPU活跃的信息,该文件中的所有值都是从系统启动开始累计到当前时刻。

[root@localhost ~]# cat /proc/self/statm

654 57 44 0 0 334 0

输出解释

CPU 以及CPU0的每行的每个参数意思(以第一行为例)为:

参数 解释 /proc/pid/statm

Size (pages) 任务虚拟地址空间的物理内存页数 

Resident(pages) 应用程序正在使用的物理内存页数 

Shared(pages) 共享页数 0

Trs(pages) 程序所拥有的可执行虚拟内存的物理内存页数 

Lrs(pages) 被映像到任务的虚拟内存空间的库的物理内存页数 

Drs(pages) 程序数据段和用户态的栈的物理内存页数 

dt(pages) 0

linux下page的大小一般为4096,即4KB

查看linux下page大小的命令是 getconf PAGE_SIZE

打开 /proc/pid/statm 文件 即可获取进程pid下包含了所有CPU活跃的信息,该文件中的所有值都是从系统启动开始累计到当前时刻。
[root@localhost ~]# cat /proc/self/statm
654 57 44 0 0 334 0
输出解释
CPU 以及CPU0的每行的每个参数意思(以第一行为例)为:
参数 解释 /proc/pid/statm
Size (pages) 任务虚拟地址空间的物理内存页数 
Resident(pages) 应用程序正在使用的物理内存页数 
Shared(pages) 共享页数 0
Trs(pages) 程序所拥有的可执行虚拟内存的物理内存页数 
Lrs(pages) 被映像到任务的虚拟内存空间的库的物理内存页数 
Drs(pages) 程序数据段和用户态的栈的物理内存页数 
dt(pages) 0
linux下page的大小一般为4096,即4KB
查看linux下page大小的命令是 getconf PAGE_SIZE
打开 /proc/pid/statm 文件 即可获取进程pid下的内存使用情况的内存使用情况




linux 下面查看内存有多种渠道,比如通过命令 ps ,top,free 等,比如通过/proc系统,一般需要比较详细和精确地知道整机内存/某个进程内存的使用情况,最好通过/proc 系统,下面介绍/proc系统下内存相关的几个文件

 

单个进程的内存查看  cat /proc/[pid] 下面有几个文件: maps , smaps, status

 

maps 文件可以查看某个进程的代码段、栈区、堆区、动态库、内核区对应的虚拟地址,如果你还不了解linux进程的内存空间,可以参考。

下图是maps文件内存示例

复制代码
 Develop>cat /proc/self/maps 00400000-0040b000 r-xp 00000000 fd:00 48 /mnt/cf/orig/root/bin/cat
0060a000-0060b000 r--p 0000a000 fd:00 48 /mnt/cf/orig/root/bin/cat
0060b000-0060c000 rw-p 0000b000 fd:00 48 /mnt/cf/orig/root/bin/cat 代码段
0060c000-0062d000 rw-p 00000000 00:00 0 [heap] 堆区
7f1fff43b000-7f1fff5d4000 r-xp 00000000 fd:00 861 /mnt/cf/orig/root/lib64/libc-2.15.so
7f1fff5d4000-7f1fff7d3000 ---p 00199000 fd:00 861 /mnt/cf/orig/root/lib64/libc-2.15.so
7f1fff7d3000-7f1fff7d7000 r--p 00198000 fd:00 861 /mnt/cf/orig/root/lib64/libc-2.15.so
7f1fff7d7000-7f1fff7d9000 rw-p 0019c000 fd:00 861 /mnt/cf/orig/root/lib64/libc-2.15.so
7f1fff7d9000-7f1fff7dd000 rw-p 00000000 00:00 0 7f1fff7dd000-7f1fff7fe000 r-xp 00000000 fd:00 2554 /mnt/cf/orig/root/lib64/ld-2.15.so
7f1fff9f9000-7f1fff9fd000 rw-p 00000000 00:00 0 7f1fff9fd000-7f1fff9fe000 r--p 00020000 fd:00 2554 /mnt/cf/orig/root/lib64/ld-2.15.so
7f1fff9fe000-7f1fff9ff000 rw-p 00021000 fd:00 2554 /mnt/cf/orig/root/lib64/ld-2.15.so
7f1fff9ff000-7f1fffa00000 rw-p 00000000 00:00 0 7fff443de000-7fff443ff000 rw-p 00000000 00:00 0 [stack] 用户态栈区
7fff443ff000-7fff44400000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] 内核区
复制代码

有时候可以通过不断查看某个进程的maps文件,通过查看其虚拟内存(堆区)是否不停增长来简单判断进程是否发生了内存溢出。

maps文件只能显示简单的分区,smap文件可以显示每个分区的更详细的内存占用数据

下图是smaps文件内存示例, 实际显示内容会将每一个区都显示出来,下面我只拷贝了代码段和堆区,

每一个区显示的内容项目是一样的,smaps文件各项含义可以参考

复制代码
 Develop>cat /proc/self/smaps 00400000-0040b000 r-xp 00000000 fd:00 48 /mnt/cf/orig/root/bin/cat
Size: 44 kB 虚拟内存大小
Rss: 28 kB 实际使用物理内存大小
Pss: 28 kB
Shared_Clean: 0 kB 页面被改,则是dirty,否则是clean,页面引用计数>1,是shared,否则是private
Shared_Dirty: 0 kB
Private_Clean: 28 kB
Private_Dirty: 0 kB
Referenced: 28 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB  处于交换区的页面大小
KernelPageSize: 4 kB  操作系统一个页面大小
MMUPageSize: 4 kB  体系结构MMU一个页面大小 
Locked: 0 kB

0060c000-0062d000 rw-p 00000000 00:00 0 [heap]
Size: 132 kB
Rss: 8 kB
Pss: 8 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 8 kB
Referenced: 8 kB
Anonymous: 8 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB

复制代码

 

下图是status文件内存示例, 加粗部分是内存相关的统计,

 

复制代码
 Develop>cat /proc/24475/status Name:    netio   可执行程序的名字
State:    R (running) 任务状态,运行/睡眠/僵死
Tgid: 24475  线程组号 Pid: 24475   进程id PPid: 19635  父进程id TracerPid: 0  Uid: 0 0 0 0 Gid: 0 0 0 0 FDSize: 256 该进程最大文件描述符个数 Groups: 0 VmPeak: 6330708 kB  内存使用峰值

VmSize: 268876 kB 进程虚拟地址空间大小

VmLck: 0 kB 进程锁住的物理内存大小,锁住的物理内存无法交换到硬盘

VmHWM: 16656 kB

VmRSS: 11420 kB 进程正在使用的物理内存大小

VmData: 230844 kB 进程数据段大小

VmStk: 136 kB 进程用户态栈大小

VmExe: 760 kB 进程代码段大小

VmLib: 7772 kB 进程使用的库映射到虚拟内存空间的大小

VmPTE: 120 kB 进程页表大小
VmSwap: 0 kB Threads: 5 SigQ: 0/63346 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000001000000 SigCgt: 0000000180000000 CapInh: 0000000000000000 CapPrm:    ffffffffffffffff
CapEff:    ffffffffffffffff
CapBnd:    ffffffffffffffff
Cpus_allowed: 01 Cpus_allowed_list: 0 Mems_allowed: 01 Mems_allowed_list: 0 voluntary_ctxt_switches: 201 nonvoluntary_ctxt_switches: 909
复制代码

可以看到,linux下内存占用是一个比较复杂的概念,不能

简单通过一个单一指标就判断某个程序“内存消耗”大小,原因有下面2点:

  • 进程所申请的内存不一定真正会被用到(malloc或mmap的实现)
  • 真正用到了的内存也不一定是只有该进程自己在用 (比如动态共享库)

关于内存的使用分析及本文几个命令的说明也可以参考这里

下面是查看整机内存使用情况的文件 /proc/meminfo

复制代码
 Develop>cat /proc/meminfo 
MemTotal: 8112280 kB 所有可用RAM大小 (即物理内存减去一些预留位和内核的二进制代码大小) MemFree: 4188636 kB LowFree与HighFree的总和,被系统留着未使用的内存 Buffers: 34728 kB 用来给文件做缓冲大小 Cached: 289740 kB 被高速缓冲存储器(cache memory)用的内存的大小
                           (等于 diskcache minus SwapCache ) SwapCached: 0 kB 被高速缓冲存储器(cache memory)用的交换空间的大小   已经被交换出来的内存,但仍然被存放在swapfile中。
                            用来在需要的时候很快的被替换而不需要再次打开I/O端口 Active: 435240 kB 在活跃使用中的缓冲或高速缓冲存储器页面文件的大小,
                              除非非常必要否则不会被移作他用 Inactive: 231512 kB 在不经常使用中的缓冲或高速缓冲存储器页面文件的大小,可能被用于其他途径. Active(anon): 361252 kB 
Inactive(anon): 120688 kB
Active(file): 73988 kB
Inactive(file): 110824 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB 交换空间的总大小 SwapFree: 0 kB 未被使用交换空间的大小 Dirty: 0 kB 等待被写回到磁盘的内存大小 Writeback: 0 kB 正在被写回到磁盘的内存大小 AnonPages: 348408 kB 未映射页的内存大小 Mapped: 33600 kB 已经被设备和文件等映射的大小 Shmem: 133536 kB 
Slab: 55984 kB 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗 SReclaimable: 25028 kB 可收回Slab的大小 SUnreclaim: 30956 kB 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab) KernelStack: 1896 kB 内核栈区大小
PageTables: 8156 kB 管理内存分页页面的索引表的大小 NFS_Unstable: 0 kB 不稳定页表的大小 Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 2483276 kB
Committed_AS: 1804104 kB
VmallocTotal: 34359738367 kB 可以vmalloc虚拟内存大小 VmallocUsed: 565680 kB 已经被使用的虚拟内存大小 VmallocChunk: 34359162876 kB
HardwareCorrupted: 0 kB
HugePages_Total: 1536  大页面数目 HugePages_Free: 0 空闲大页面数目 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB 大页面一页大小
DirectMap4k: 10240 kB 
DirectMap2M: 8302592 kB
复制代码

 

 

想必在linux上写过程序的同学都有分析进程占用多少内存的经历,或者被问到这样的问题——你的程序在运行时占用了多少内存(物理内存)?通常我们可以通过top命令查看进程占用了多少内存。这里我们可以看到VIRT、RES和SHR三个重要的指标,他们分别代表什么意思呢?这是本文需要跟大家一起探讨的问题。当然如果更加深入一点,你可能会问进程所占用的那些物理内存都用在了哪些地方?这时候top命令可能不能给到你你所想要的答案了,不过我们可以分析proc文件系统提供的smaps文件,这个文件详尽地列出了当前进程所占用物理内存的使用情况。

    这篇blog总共分为三个部分。第一部分简要阐述虚拟内存和驻留内存这两个重要的概念;第二部分解释top命令中VIRT、RES以及SHR三个参数的实际参考意义;最后一部分向大家介绍一下smaps文件的格式,通过分析smaps文件我们可以详细了解进程物理内存的使用情况,比如mmap文件占用了多少空间、动态内存开辟消耗了多少空间、函数调用栈消耗了多少空间等等。

关于内存的两个概念

      要理解top命令关于内存使用情况的输出,我们必须首先搞清楚虚拟内存(Virtual Memory)驻留内存(Resident Memory)两个概念。

  •  虚拟内存

   首先需要强调的是虚拟内存不同于物理内存,虽然两者都包含内存字眼但是它们属于两个不同层面的概念。进程占用虚拟内存空间大并非意味着程序的物理内存也一定占用很大。虚拟内存是操作系统内核为了对进程地址空间进行管理(process address space management)而精心设计的一个逻辑意义上的内存空间概念。我们程序中的指针其实都是这个虚拟内存空间中的地址。比如我们在写完一段C++程序之后都需要采用g++进行编译,这时候编译器采用的地址其实就是虚拟内存空间的地址。因为这时候程序还没有运行,何谈物理内存空间地址?凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中。既然说虚拟内存是一个逻辑意义上(假象的)的内存空间,为了能够让程序在物理机器上运行,那么必须有一套机制可以让这些假象的虚拟内存空间映射到物理内存空间(实实在在的RAM内存条上的空间)。这其实就是操作系统中页映射表(page table)所做的事情了。内核会为系统中每一个进程维护一份相互独立的页映射表。。页映射表的基本原理是将程序运行过程中需要访问的一段虚拟内存空间通过页映射表映射到一段物理内存空间上,这样CPU访问对应虚拟内存地址的时候就可以通过这种查找页映射表的机制访问物理内存上的某个对应的地址。“页(page)”是虚拟内存空间向物理内存空间映射的基本单元。

        下图1演示了虚拟内存空间和物理内存空间的相互关系,它们通过Page Table关联起来。其中虚拟内存空间中着色的部分分别被映射到物理内存空间对应相同着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。这么做也是本着“按需映射”的指导思想,因为虚拟内存空间很大,可能其中很多部分在一次程序运行过程中根本不需要访问,所以也就没有必要将虚拟内存空间中的这些部分映射到物理内存空间上。

        到这里为止已经基本阐述了什么是虚拟内存了。总结一下就是,虚拟内存是一个假象的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间中。虚拟内存空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。

                  图1. 虚拟内存空间到物理内存空间映射

  •  驻留内存

  驻留内存,顾名思义是指那些被映射到进程虚拟内存空间的物理内存。上图1中,在系统物理内存空间中被着色的部分都是驻留内存。比如,A1、A2、A3和A4是进程A的驻留内存;B1、B2和B3是进程B的驻留内存。进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虚拟内存大并不意味着占用的物理内存大。

  关于虚拟内存和驻留内存这两个概念我们说到这里。下面一部分我们来看看top命令中VIRT、RES和SHR分别代表什么意思。

top命令中VIRT、RES和SHR的含义

     搞清楚了虚拟内存的概念之后解释VIRT的含义就很简单了。VIRT表示的是进程虚拟内存空间大小。对应到图1中的进程A来说就是A1、A2、A3、A4以及灰色部分所有空间的总和。也就是说VIRT包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分总和。

  RES的含义是指进程虚拟内存空间中已经映射到物理内存空间的那部分的大小。对应到图1中的进程A来说就是A1、A2、A3以及A4几个部分空间的总和。所以说,看进程在运行过程中占用了多少内存应该看RES的值而不是VIRT的值。

  最后来看看SHR所表示的含义。SHR是share(共享)的缩写,它表示的是进程占用的共享内存大小。在上图1中我们看到进程A虚拟内存空间中的A4和进程B虚拟内存空间中的B3都映射到了物理内存空间的A4/B3部分。咋一看很奇怪。为什么会出现这样的情况呢?其实我们写的程序会依赖于很多外部的动态库(.so),比如libc.so、libld.so等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映射到对应进程的虚拟内存空间中。多个进展之间通过共享内存的方式相互通信也会出现这样的情况。这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,所以我们将他们称为共享内存,用SHR来表示。某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可。

进程的smaps文件

  通过top命令我们已经能看出进程的虚拟空间大小(VIRT)、占用的物理内存(RES)以及和其他进程共享的内存(SHR)。但是仅此而已,如果我想知道如下问题:

  1. 进程的虚拟内存空间的分布情况,比如heap占用了多少空间、文件映射(mmap)占用了多少空间、stack占用了多少空间?
  2. 进程是否有被交换到swap空间的内存,如果有,被交换出去的大小?
  3. mmap方式打开的数据文件有多少页在内存中是脏页(dirty page)没有被写回到磁盘的?
  4. mmap方式打开的数据文件当前有多少页面已经在内存中,有多少页面还在磁盘中没有加载到page cahe中?
  5. 等等

  以上这些问题都无法通过top命令给出答案,但是有时候这些问题正是我们在对程序进行性能瓶颈分析和优化时所需要回答的问题。所幸的是,世界上解决问题的方法总比问题本身要多得多。linux通过proc文件系统为每个进程都提供了一个smaps文件,通过分析该文件我们就可以一一回答以上提出的问题。

  在smaps文件中,每一条记录(如下图2所示)表示进程虚拟内存空间中一块连续的区域。其中第一行从左到右依次表示地址范围、权限标识、映射文件偏移、设备号、inode、文件路径。详细解释可以参见understanding-linux-proc-id-maps

  接下来8个字段的含义分别如下:

  • Size:表示该映射区域在虚拟内存空间中的大小。
  • Rss:表示该映射区域当前在物理内存中占用了多少空间      
  • Shared_Clean:和其他进程共享的未被改写的page的大小
  • Shared_Dirty: 和其他进程共享的被改写的page的大小
  • Private_Clean:未被改写的私有页面的大小。
  • Private_Dirty: 已被改写的私有页面的大小。
  • Swap:表示非mmap内存(也叫anonymous memory,比如malloc动态分配出来的内存)由于物理内存不足被swap到交换空间的大小。
  • Pss:该虚拟内存区域平摊计算后使用的物理内存大小(有些内存会和其他进程共享,例如mmap进来的)。比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB,那么该进程分摊其中一半的内存,即Pss=500KB。

                            图2. smaps文件中的一条记录

  有了smap如此详细关于虚拟内存空间到物理内存空间的映射信息,相信大家已经能够通过分析该文件回答上面提出的4个问题。

  最后希望所有读者能够通过阅读本文对进程的虚拟内存和物理内存有一个更加清晰认识,并能更加准确理解top命令关于内存的输出,最后可以通过smaps文件更进一步分析进程使用内存的情况。

阅读(12034) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~