Linux裁剪應用內存的一次實踐

背景

在嵌入式設備,總會出現內存不夠用的情況。我就遇到一個場景,需要把某個應用塞到資源有限的 Linux 設備中。這應用進程所需要的內存已然超過了 Linux 設備所能提供的空閒內存。

在我的場景中,應用跑起來後共享內存佔所有物理內存的 70% 了。與其死摳應用代碼,少申請內存,還不如想辦法轉用其他更少資源的共享庫。

所以本文並不會涉及到應用實現上如何更少的內存使用,更多是分享如何從 Linux 系統角度分析應用內存的分佈和使用情況。

對我而言,則是又一次理論指導實踐的過程,嗯,理論落地的感覺真好。

出於保密的需要,本文一些數據和日誌等都會用無意義字符替代,但不會影響同學們閱讀和理解

內存的基本知識

在 Linux 上,應用使用的內存可以分爲兩類,分別是 虛擬內存 和 物理內存。應用可以肆意申請內存,在不超過尋址範圍的情況下,很少會有申請失敗的情況,畢竟對 Linux 來說並沒有分配物理內存。直至你真的要操作申請的內存了,Linux 才真切分配物理內存。關於虛擬內存和物理內存的概念本文不闡述,我只是爲了說明,分析內存佔用,不能看虛擬內存,必須看物理內存。

由於在 Linux 上,有一些內存是共享的,主要集中在應用加載的動態庫上。就好像最常用的 C 庫,應用打印個 “helloworld” 可都要鏈接到 C 庫。每一個應用都加載一份 C 庫多浪費啊,杜絕浪費,從你我做起。於是 Linux 就只會加載一次 C 庫,然後映射到不同應用的虛擬地址,實現不同應用(進程)之間“共享”一份動態庫。這不是完全意義上的共享,畢竟動態庫裏的全局變量對每個進程而言都是私有的。正因爲共享動態庫的存在,我們統計應用(進程)使用的物理內存時,是否包含以及如何包含共享的動態庫佔用的物理內存,就產生了2個新的概念:PSSUSS

準確來說,跟 PSSUSS 相似的詞彙還有 VSSRSS。網上有不少資料,我找到這麼一篇文章介紹非常生動:《linux中top命令 VSS,RSS,PSS,USS 四個內存字段的解讀。》,不懂的同學自己看資料哈,我這裏做個簡單的描述:

  • VSS:Virtual Set Size

    就是虛擬內存,包括進程已經申請,雖然不一定分配了物理內存,但進程訪問不會觸發段錯誤的內存。

  • RSS:Resident Set Size

    就是所有的物理內存,會統計上用到的動態庫的所有內存,即使這動態庫多個進程共享。

  • PSS:Proportional Set Size

    也是物理內存,只不過所有動態庫的內存是按比例計算,例如 N 個進程鏈接動態庫,那麼每個進程只會計算佔用動態庫 1/N 的空間。

  • USS:Unique Set Size

    也是物理內存,但不包含動態庫的內存,也就是進程私有的內存。

查看進程內存使用情況

如果要查看系統內存使用情況,常用 free 命令。但在這裏,我們需要精細到進程爲單位的內存使用情況。補充一點,所有線程共用一個mm_struct實例,因此內存使用情況以進程爲單位,無法再細分到線程。

那麼如何查看進程的內存使用情況呢?在 PC 上我們可以使用 top,或者 ps,例如:

$ top
top - xx:xx:xx up xxx days, xx:xx, x users,  load average: 1.31, 1.50, 2.52
Tasks: xxx total,   x running, xxx sleeping,  xx stopped,   x zombie
%Cpu(s):  1.4 us,  0.4 sy,  0.0 ni, 98.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : xxxx total,  xxxx free, xxxx used, xxxx buff/cache
KiB Swap:   xxxx total,   xxxx free,        0 used. xxxx avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
35370 xxxxx     20   0 xxxxxxx xxxxxx  xxxxx S   3.9  0.3   1:38.22 xxx
39464 xxxxx     20   0   xxxxx  xxxxx   xxxx S   1.6  0.0   0:00.64 xxx
....

上面的 VIRT 就是虛擬內存使用情況,等效於 VSS;RES 是物理內存,等效於 RSS;SHR 是共享內存。

但在大多數嵌入式設備上,top或者ps都是閹割版本的,顯示的內容不全,非常無奈,例如我在 openWRT 上執行ps,只會顯示虛擬內存:

# ps
root@xxxx:/# ps w
  PID USER       VSZ STAT COMMAND
    1 root      3028 S    /sbin/procd
    2 root         0 SW   [kthreadd]
....

除了使用第三方的htop之外,在一個連ps/top都沒有的非常極端的環境,我們可以直接查詢 procfs,簡單粗暴。如何做呢?

cat /proc/<pid>/statm

實際上,從 htop 的源碼發現,其也是從 statm 獲取的進程內存使用情況。例如,我要查看進程 1 的內存使用情況:

$ cat /proc/1/statm
57 502 363 14 0 181 0

這幾個數據什麼意思呢?查看 Linux 的 Documentation,有如下的註釋:

$ cat filesystems/proc.txt
...
Table 1-3: Contents of the statm files (as of 2.6.8-rc3)
..............................................................................
 Field    Content
 size     total program size (pages)        (same as VmSize in status)
 resident size of memory portions (pages)   (same as VmRSS in status)
 shared   number of pages that are shared   (i.e. backed by a file, same
                        as RssFile+RssShmem in status)
 trs      number of pages that are 'code'   (not including libs; broken,
                            includes data segment)
 lrs      number of pages of library        (always 0 on 2.6)
 drs      number of pages of data/stack     (including libs; broken,
                            includes library text)
 dt       number of dirty pages         (always 0 on 2.6)
...

翻譯過來就是:

第一個字段:Size : 任務虛擬地址空間的大小(單位:pages)
第二個字段:Resident:應用程序正在使用的物理內存的大小(單位:pages)
第三個字段:Shared: 共享頁數(單位:pages)
第四個字段:Trs:程序所擁有的可執行虛擬內存的大小(單位:pages)
第五個字段:Lrs:被映像到任務的虛擬內存空間的庫的大小(單位:pages)
第六個字段:Drs:程序數據段和用戶態的棧的大小(單位:pages)
第七個字段:dt:髒頁的數量

一般情況下,pages 的大小是 4k ,因此上述的結果除以 4 得到的值單位是 KB。在上例中:

$ cat /proc/1/statm
57 502 363 14 0 181 0

# 表示
# 57: 虛擬內存:57/4 = 14 KB
# 502:物理內存(RSS): 502/4 = 125 KB
# ...

查看進程內存的分佈

只是知道進程內存的分佈還不能滿足我場景的需求。在我的案例中,70%的物理內存都是共享內存,我還需要細緻到用了什麼庫,每個庫用了多少物理內存,我才知道該優化什麼庫。

要查看虛擬內存的分佈情況,可以 cat /proc/<pid>/maps,例如:

# cat /proc/1/maps
00400000-0040e000 r-xp 00000000 b3:05 628                                /sbin/procd
0041e000-0041f000 r--p 0000e000 b3:05 628                                /sbin/procd
0041f000-00420000 rw-p 0000f000 b3:05 628                                /sbin/procd
00420000-00422000 rw-p 00000000 00:00 0
2e7dd000-2e853000 rw-p 00000000 00:00 0                                  [heap]
7fa86f9000-7fa881c000 r-xp 00000000 b3:05 445                            /lib/libc-2.23.so
7fa881c000-7fa882b000 ---p 00123000 b3:05 445                            /lib/libc-2.23.so
7fa882b000-7fa882f000 r--p 00122000 b3:05 445                            /lib/libc-2.23.so
7fa882f000-7fa8831000 rw-p 00126000 b3:05 445                            /lib/libc-2.23.so
...

但還不夠,這隻知道了每個庫隱射到哪段虛擬內存,我還需要知道實際的物理內存。此時需要cat /proc/<pid>/smaps

smaps在我的板子上找不到,在內核中檢索代碼實現,發現需要在內核中使能CONFIG_PROC_PAGE_MONITOR。使能後重新編譯內核,我們就可以獲取實際的物理內存啦。例如:

# cat /proc/1/smaps
...
7fa8908000-7fa890e000 r-xp 00000000 b3:05 485                            /lib/librt-2.23.so
Size:                 24 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                  24 kB
Pss:                   6 kB
Shared_Clean:         24 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:           24 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
VmFlags: rd ex mr mw me
...

上例子中,librt 的庫有映射到虛擬內存 7fa8908000-7fa890e000,權限是 r-xp,虛擬內存是 24kB,RSS 是 24KB,PSS 是 6KB。

數據分析

通過以下的命令可以初步過濾 smap 的 RSS 和 PSS,再根據 PSS 排序,取 PSS 使用量最大的前20個:

cat /proc/<pid>/smaps | awk '{
    if ($1 ~ /^[[:digit:]]+/)
        printf "<%s> : ", $6
    if ($1 ~ /^Rss/)
        printf "RSS = %sKB; ", $2;
    if ($1 ~ /^Rss/)
        printf "PSS = %sKB\n", $2;
}' | sort -hrk 8 | head -n 20

結果如下:

...
</usr/lib/libXXXXXXXXX.so> : RSS = 1544KB; PSS = 1544KB
</usr/lib/libXXXXXXXX.so> : RSS = 1172KB; PSS = 1172KB
</usr/lib/libXXXXXX.so> : RSS = 1148KB; PSS = 1148KB
...
</lib/libXXXX.so> : RSS = 1072KB; PSS = 1072KB
</lib/libXXXX.so> : RSS = 1060KB; PSS = 1060KB
</usr/lib/libXXXX.so.30.23.0> : RSS = 824KB; PSS = 824KB
<[heap]> : RSS = 692KB; PSS = 692KB
</usr/lib/libXXXX.so> : RSS = 664KB; PSS = 664KB
</usr/lib/libXXXXX.so.2.0.0> : RSS = 640KB; PSS = 640KB
...

由於保密的需要,我就不完整貼出來了。根據統計的結果,PPS 使用量前 20 的動態庫竟然佔了進程共享內存的90%的物理內存,非常誇張。

採用一些相同功能,卻更小巧的動態庫還是有很大效果的。此外,通過這個方法也可以知道堆和棧對內存使用的情況,在做應用內存優化的時候也可以有個側重方向。

優化還在持續進行中,結果就不貼出來了。

總結

本文介紹了 Linux 內存的一些基本理論知識,重點還是在於如何把理論付諸實踐。通過這次的應用內存裁剪的分析過程,重溫了 VSS、RSS、USS、PSS,以及學習瞭如何在 Linux 上查詢進程的內存使用情況,如何查詢虛擬內存的分佈,以及進程各個動態庫、堆、棧的物理內存使用情況對應用內存裁剪的指導意義。

對大多數人來說,通過top/ps查看物理內存就到底了吧。平時注意理論積累,在關鍵的時候纔不會像無頭蒼蠅。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章