[譯] C程序員該知道的內存知識 (4)

系列更新:node

這是本系列的第4篇,也是最後一篇,含淚填完這個坑不容易,感謝閱讀~程序員

這個系列太乾了,閱讀量一篇比一篇少,但我仍然認爲這個系列很是有價值,在翻譯的過程當中我也藉機進行系統性的梳理、並學習了不少新知識,收穫滿滿。但願你也能有收穫(但確定沒我多)。面試

那,開始吧。shell


理解內存消耗

工具箱:編程

  • vmtouch[2] - portable virtual memory toucher

(譯註:vmtouch這個工具用來診斷和控制系統對文件系統的緩存,例如查看某個文件被緩存了多少頁,清空某個文件的緩存頁,或將某個文件的頁面鎖定在內存中;基於這些功能能夠實現不少有意思的應用;詳情參考該工具的文檔。) segmentfault

然而共享內存的概念致使傳統方案 —— 測量對內存的佔用 —— 變得無效了,由於沒有一個公正的方法能夠測量你進程的獨佔空間。這會引發困惑甚至恐懼,多是兩方面的:api

用上了基於 mmap 的I/O操做後,咱們的應用如今幾乎不佔用內存.

— CorporateGuy緩存

求救!我這寫入共享內存的進程有嚴重的內存泄漏!!!bash

— HeavyLifter666app

頁面有兩種狀態:清潔(clean)頁和髒(dirty)頁。區別是,髒頁在被回收以前須要被寫回到持久存儲中(譯註:寫回文件實際存放的地方)。MADV_FREE 這個建議經過將髒標誌位清零這種方式來實現更輕量的內存釋放,而不是修改整個頁表項(譯註:page table entry,常縮寫爲PTE,記錄頁面的物理頁號及若干標誌位,如可否讀寫、是否髒頁、是否在內存中等)。此外,每一頁均可能是私有的或共享的,這正是致使困惑的源頭。

前面引用的兩個都是(部分)真實的,取決於視角。在系統緩衝區的頁面須要計入進程的內存消耗裏嗎?若是進程修改了緩衝區裏那些映射文件的那些頁面呢?在這混亂中能夠整出點有用的東西麼?

假設有一個進程,索倫之眼(the_eye) 會寫入對魔都(mordor)的共享映射(譯註:指環王的梗)。寫入共享內存不計入 RSS(resident set size,常駐內存集)的,對吧?

$ ps -p $$ -o pid,rss
  PID  RSS
17906  1574944 # <-- 什麼鬼? 佔用1.5GB?

(譯註:$$ 是 bash 變量,保存了在執行當前script的shell的PID;這裏應該是用來指代the_eye的PID)

呃,讓咱們回到小黑板。

PSS(Proportional Set Size)

PSS(譯註:Proportional 意思是 「比例的」) 計入了私有映射,以及按比例計入共享映射。這是咱們能獲得的最合理的內存計算方式了。關於「比例」,是指將共享內存除以共享它的進程數量。舉個例子,有個應用須要讀寫某個共享內存映射:

$ cat /proc/$$/maps
00400000-00410000         r-xp 0000 08:03 1442958 /tmp/the_eye
00bda000-01a3a000         rw-p 0000 00:00 0       [heap]
7efd09d68000-7f0509d68000 rw-s 0000 08:03 4065561 /tmp/mordor.map
7f0509f69000-7f050a108000 r-xp 0000 08:03 2490410 libc-2.19.so
7fffdc9df000-7fffdca00000 rw-p 0000 00:00 0       [stack]
... 如下截斷 ...

(譯註:cat /proc/$PID/maps 是從內核中讀取進程的全部內存映射)

這是個被簡化並截斷了的映射,第一列是地址範圍,第二列是權限信息,其中 r 表示可讀, w 表示可寫,x 表示可執行 —— 這都是老知識點了 —— 而後 s 表示共享,p 表示私有。而後是映射文件的偏移量,設備號(OS分配的),inode號(文件系統上的),以及最後是文件的路徑名。具體參見這個文檔[3](譯註:kernel.org 對 /proc 文件系統的說明文檔),超級詳細。

我得認可我刪掉了一些輸出中一些不太有意思的信息。若是你對被私有映射的庫感興趣的話能夠讀一下 FAQ-爲何「strict overcommit」是個蠢主意[4](譯註:根據這個FAQ,strict overcommit應該是指容許overcommmit、但要爲申請的每個虛擬頁分配一個真實頁,不論是用物理頁仍是swap,確實聽起來很蠢……)。不過這裏咱們感興趣的是魔都(mordor)這個映射:

$ grep -A12 mordor.map /proc/$$/smaps
Size:           33554432 kB
Rss:             1557632 kB
Pss:             1557632 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:   1557632 kB
Private_Dirty:         0 kB
Referenced:      1557632 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Locked:                0 kB
VmFlags: rd wr sh mr mw me ms sd

譯註:這個文件大小 32GB,已加載了 1521MB 到內存中,由於只有這一個進程映射了它,因此在這個進程的PSS中佔比是100%,也是 1521MB。

在共享映射裏的私有頁面 —— 搞得我像巫師同樣?在Linux上,即便共享內存也會被認爲是私有的,除非它真的被共享了(譯註:不止一個進程建立共享映射)。讓咱們看看它是否在系統緩衝區裏:

# 好像開頭的那一塊在內存中...
$ vmtouch -m 64G -v mordor.map
[OOo                    ] 389440/8388608

           Files: 1
     Directories: 0
  Resident Pages: 389440/8388608  1G/32G  4.64%
         Elapsed: 0.27624 seconds

# 將它全都載入到Cache!
$ cat mordor.map > /dev/null
$ vmtouch -m 64G -v mordor.map
[ooooooooo      oooOOOOO] 2919606/8388608

           Files: 1
     Directories: 0
  Resident Pages: 2919606/8388608  11G/32G  34.8%
         Elapsed: 0.59845 seconds

譯註:

  1. 「-m 64G」 表示容許 vmtouch 將小於 64G 的文件加載到內存中,應當是用於須要加載一個目錄下的文件、但排除其中過大的文件,彷佛不適用於這裏;至少忽略這個參數不影響閱讀
  2. o 表示這一塊部分被加載,O 表示所有被加載。由於物理內存有限,雖然全量讀取了文件,但只有部份內容被緩存

嗬,只是簡單地讀取一個文件就會把它緩存起來?先無論這,咱們的進程呢?

$ ps -p $$ -o pid,rss
  PID   RSS
17906 286584 # <-- 等了足足一分鐘

常見的誤解是,映射文件會消耗內存,而經過文件API讀取不會。實際上,不管哪種方式,包含文件內容的頁面都會被放進系統緩衝區。但還有個小的區別是,使用mmap的方式須要在進程的頁表中建立對應的頁表項(PTE),而這些包含文件內容的頁面是能夠被共享的。有趣的是,咱們這個進程的RSS縮小了,由於系統 _須要_ 進程的頁面了(譯註:由於 mordor 太大,可用物理內存頁不夠,系統將 the_eye 的部分頁面swap了;因此前述命令纔會須要等一分鐘,由於涉及到磁盤IO)。

有時咱們的全部想法都是錯的

映射文件的內存老是可被回收的,區別只在於該頁是否髒頁 —— 髒頁在回收前須要被清理(譯註:寫回底層存儲)。因此當你在 top 命令發現有一個進程佔用了大量內存時是否須要恐慌?當這個進程有不少匿名的髒頁的時候才須要恐慌——由於這些頁面沒法被回收。若是你發現有個匿名映射段在增加,你可能就有麻煩了(並且是雙倍的麻煩)。可是不要盲目相信 RSS 甚至 PSS 。

另外一個常見錯誤是認爲進程的虛擬內存和實際消耗內存之間總有某種關係,甚至認爲全部內存映射都同樣。任何可回收的內存,實際上均可以認爲是空閒的。簡而言之,它不會致使你下次內存分配失敗,但_可能_會增長分配的延遲 —— 這點我會解釋:

內存管理器須要花很大功夫來決定哪些東西須要保存在物理內存裏。它可能會決定將進程內存中的一部分調到swap,以便給系統緩存騰出空間,所以該進程下次訪問這一塊時須要再將這些頁面調回到物理內存中。幸運的是這一般是能夠配置的。例如,Linux 有一個叫作 swappiness[5] 的選項,用來指導內核什麼時候開始將匿名映射的內存頁調出到swap。當它取值爲 0 是表示「直到絕對絕對有必要的時候」(譯註:取值[0, 100],值越低,系統越傾向於先清理系統緩衝區的頁面)。

終章,一勞永逸地

若是你看到這裏,向你致敬!我在工做之餘寫的這篇文章,但願能用一種更方便的方式,不只能解釋這些說過上千遍的概念,還能幫我整理這些思惟,以及幫助其餘人。我花了比預期更長的時間。遠超預期。

我對文章的做者們只有無盡的敬意,由於寫做真是個冗長乏味、使人頭禿的過程,須要永無止境的修改和重寫。Jeff Atwood(譯註:stack overflow的創始人)曾說過,最好的學編程書籍是教你蓋房子的那本。我不記得在哪兒了,因此沒法引用它。我只能說,第二好的是教你寫做的那本。說到底,編程本質上就是寫故事,簡明扼要。

EDIT:我修正了關於 alloca() 和 將 sizeof(char) 誤寫爲 sizeof(char*) 的錯誤,多虧了 immibis 和 BonzaiThePenguin。感謝 sWvich 指出在 slab + sizeof(struct slab) 裏漏了的類型轉換。顯然我應該用靜態分析跑一下這篇文章,但並無 —— 漲經驗了。

開放問題 —— 有沒有比 Markdown 代碼塊更好的實現?我但願能展現帶註釋的摘錄,而且能下載整個代碼塊。

寫於 2015 年 2 月 20 日。



讀到這裏都是真愛,喜歡的話請點贊,感謝~

照例再貼下以前推送的幾篇文章:

歡迎關注

weixin1.png


參考連接:

[1] What a C programmer should know about memory

[2] vmtouch - the Virtual Memory Toucher

[3] kernel.org - THE /proc FILESYSTEM

[4] FAQ (Why is 「strict overcommit」 a dumb idea?)

[5] wikipedia - Paging - swapinness

相關文章
相關標籤/搜索