認真分析下進程的內存,說說VSZ、RSS、PSS

進程占用的內存可以有以下這些類型:

  1. 自身的代碼
  2. 共享庫的代碼
  3. 運行過程分配的堆和棧
  4. 通過mmap映射的磁盤文件內容

1. 虛擬內存與物理內存

這里要區分兩個概念,虛擬內存和物理內存。物理內存對于進程來說是透明的,進程直接操作的是虛擬內存。而數據和代碼是存放在真實的物理內存的,之所以進程在虛擬內存中尋址可以獲取數據,是因為虛擬內存與物理內存存在著映射關系。

當我們的進程向系統申請內存時,比如通過malloc方法,得到的其實是虛擬內存,如果進程沒有使用這些虛擬內存,那么它們是不會和物理內存關聯起來的。比如如果我們malloc 10MB的內存,但是只用了一個byte的,那么進程實際得到的只有一個頁的物理內存,也就是4096byte的內存空間。當物理內存被換出到磁盤(swap out),虛擬內存對應的地址還是有效的,如果尋址到這些地址,對應的物理內存就會被換入到內存(swap in)。

虛擬內存是連續的,而物理內存卻不一定。

2. 共享的內存

從進程自身的角度看,虛擬內存是進程獨立的,所有內存都是私有的,包括自身代碼、共享庫、堆棧等,它不用關心共享內存的事情。但實際上在物理內存的層面,很多東西是可以共享的,比如共享的代碼庫(.so)、自身代碼甚至是自身運行時私有的堆棧內存。

2.1 共享庫

同一個共享庫的代碼在物理內存中只會存在一份,這塊內存會映射到不同進程的虛擬內存中,對各個進程來說,就像是自己私有的內存一樣,而對于系統來說,則是節省了內存的資源。

2.2 進程自身的代碼

同樣,同一份代碼運行起來的多個進程是共享這些代碼的內存的,因為可執行代碼類型的內存頁是只讀的(除非是debugger模式),沒有必要復制多份,因此在實際場景中,當我們啟動一個大應用程序后,再啟動它的另一個實例,第二次啟動會快很多,這就是因為其代碼已經在內存中了,無需再重新加載一遍。

2.3 進程自身的私有內存

如果從一個進程fork出一個子進程,那么父進程的私有內存(比如堆棧)則會與子進程共享,但會被標記成(copy-on-write),意思是如果兩個進程都沒有修改這些內存頁,那么這些內存頁在兩個進程間就是共享的,但如果某個進程要修改某個頁了,那么這個頁就會先被復制一份,再被修改。

3. 進程內存的數據統計

在OSX系統,可以運行以下命令來查看某個進程內每一塊內存的類型(mapped file、Stack內存、malloc內存、代碼的__DATA或者__TEXT段等),以及大小、是否共享或者copy-on-write等。

sudo vmmap <pid>

如果只是關注RAM的內存,可以加上-resident參數:

sudo vmmap -resident <pid>

比如指定Firefox進程,有如下輸出:

可以看到,Firefox進程的代碼和庫代碼加載了108MB__TEXT段數據,字體支持(ATS)需要33MB內存,但只有2.5MB真正加載在物理內存中,它通過MALLOC申請了256MB內存,并且247MB在物理內存中,它有14MB用于棧內存,但只用了248KB。

4. 進程內存大小的度量方法

提到進程消耗的內存大小,我們或多或少聽到VSZ、RSS、PSS,那么它們代表的是什么呢?有了上述的知識背景,現在來分析或許能更加清晰。

4.1 VSZ(Virtual Memory Size)

指的是虛擬內存的大小,進程運行理論需要的內存大小,用這個來表示進程消耗了多少內存其實沒有太大的意義,因為它包含了未被加載到實際內存中的空間。舉個例子,假如有個文本編輯器叫做emacs,它有個編輯xml文件的功能,但這個功能比較少被用到,因為用戶一般情況下是編輯普通的文本,因此沒有必要一啟動就把這個功能的代碼加載到內存中。

除非用戶真實用到某個頁,否則系統不會把這個頁加載到內存,這其實稱為demand paging feature。

來看一下上面這個例子中虛擬內存工作的流程。首先啟動應用程序,系統為進程分配了運行編輯xml所需要的虛擬內存,但并沒有真正把這些功能所在的頁加載到物理內存。當進程真正調用到編輯xml的功能,CPU上的MMU模塊將會告訴系統,對應的虛擬內存頁發生缺頁了,那么系統就會暫停運行中的進程,把對應的頁加載到內存,再把這些物理內存頁映射到虛擬內存上,最后讓應用程序從暫停的地方繼續執行。對于進程來說,它是不知道自己被暫停了的,它只需要簡單地認為對應的功能已經加載在虛擬內存上了,并使用它就好了。

虛擬內存描述了進程運行時所需要的總內存大小,包括了那些還沒有被加載到實際內存中的代碼和數據。

4.2 RSS(Resident Set Size)

RSS表示了進程中真正被加載到物理內存中的頁的大小。但是用它來表示進程占用的內存大小也不太合適,因為還有個共享代碼庫的概念(Shared Libraries)。

比如libxml2.so這個程序庫,有多個進程會用到它,而系統在物理內存只會加載一遍這個代碼庫,然后這塊物理內存會被映射到不同進程的虛擬內存空間中,對于單獨的進程來說,就像是這個庫只加載在自己的虛擬內存中一樣,不需要關心它是否與其它進程共享。

而進程的RSS是包含這塊共享庫的內存空間的,因此如果簡單把系統中所有進程的RSS相加的話,結果是比系統總的內存大的,因為共享庫占的內存被計算了多遍。

4.3 PSS(Proportional Set Size)

PSS在VSS的基礎上,將共享庫的內存按使用的進程個數平均分成多份。假如有N個進程使用libxml2.so這個庫,這個庫加載了200K代碼在內存中,那么每個進程的PSS值中有(200 / N)K 的大小是這個共享庫貢獻的。

如果把系統中所有進程的PSS值加起來,就等于系統所有進程占用的內存總大小。

但是PSS并不是在所有的Linux系統中都有提供的,比如ps命令中就沒有PSS值,而Android的adb shell dumpsys meminfo <pid>命令就可以看到進程的PSS值。

4.4 總結一下三種度量方法

如果要度量進程占用的內存大小,較好的選擇是使用PSS,用RSS也行,不過要注意有些內存是和別的進程共享的。

再舉個例子總結一下前面三個概念,比如一個進程有500K的代碼并且鏈接了2500K的共享庫,然后有200K的堆棧分配。其中有400K自身的代碼、1000K的共享庫以100K的堆棧內存被加載在實際內存(RAM)中,并且系統中一共有兩個進程用了同樣的共享庫。那么:

VSZ:500K + 2500K + 200K = 3200K
RSS:400K + 1000K + 100K = 1500K
PSS:400K + (1000K / 2) + 100K = 1000K

5. 命令執行結果

在Android的adb shell中執行ps命令的結果:

  1. 執行adb shell ps
  1. 執行adb shell dumpsys meminfo com.android.calendar

6. 參考文獻:

  1. https://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management
  2. https://web.archive.org/web/20120520221529/http://emilics.com/blog/article/mconsumption.html
  3. https://stackoverflow.com/questions/118307/a-way-to-determine-a-processs-real-memory-usage-i-e-private-dirty-rss
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容