原文地址: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手冊也是錯誤的,或者至少是不清楚的。