[轉帖]Linux效能最佳化—記憶體效能篇分享專題

济南小老虎發表於2024-05-29
https://heapdump.cn/monographic/detail/40/4554233

原理篇:記憶體管理是如何工作的?

Allenwang

與 CPU 管理一樣,記憶體管理也是作業系統的核心功能之一。記憶體主要用於儲存系統和應用程式指令、資料、快取等。

記憶體對映

我們通常所說的記憶體容量,其實是指實體記憶體。實體記憶體也稱為主記憶體,大多數計算機使用的主記憶體是動態隨機存取儲存器(DRAM)。只有核心可以直接訪問實體記憶體。那麼,程序想要訪問記憶體時應該怎麼做呢?

Linux核心為每個程序提供了一個獨立的虛擬地址空間,這個地址空間是連續的。這樣,程序就可以方便地訪問記憶體,更準確地說是虛擬記憶體。

虛擬地址空間內部分為核心空間和使用者空間兩部分。具有不同字長(單個 CPU 指令可以處理的最大資料長度)的處理器具有不同的地址空間範圍。例如,對於 32 位和 64 位系統,下圖顯示了它們的虛擬記憶體空間:

63E60A12BA5448E2BD5B49E8B7F6B88D.png
從上圖可以看出,32位系統的核心空間為1G,位於最高點,剩下的3G為使用者空間。64位系統的核心空間和使用者空間都是128T,中間的其餘部分未定義。

還記得程序的使用者模式和核心模式嗎?當一個程序處於使用者態時,它只能訪問使用者空間記憶體;只有進入核心模式後才能訪問核心空間記憶體。雖然每個程序的地址空間都包含核心空間,但這些核心空間實際上是與同一個實體記憶體相關聯的。這樣,程序切換到核心模式後,就可以輕鬆訪問核心空間記憶體。

由於每個程序都有這麼大的地址空間,所有程序加起來的虛擬記憶體自然要比實際的實體記憶體大很多。因此,並不是所有的虛擬記憶體都會分配實體記憶體,只有實際使用的虛擬記憶體才會分配實體記憶體,分配的實體記憶體是透過記憶體對映來管理的。

記憶體對映實際上是虛擬記憶體地址到實體記憶體地址的對映。為了完成記憶體對映,核心為每個程序維護一張頁表,記錄虛擬記憶體地址和實體地址的對映關係:
2.png

頁表實際上儲存在 CPU 的 MMU(記憶體管理單元)中。當在頁表中找不到程序訪問的虛擬地址時,系統會產生“缺頁異常”,進入核心空間定位實體記憶體,然後更新程序頁表,最後返回使用者空間恢復程序。

TLB(Translation Lookaside Butter)是 MMU 中的頁表快取。由於程序的虛擬地址空間是獨立的,而且TLB的訪問速度比MMU快很多,透過減少程序的上下文切換和減少TLB的重新整理次數,可以提高TLB快取的利用率進行改進,從而提高 CPU 效能。

不過需要注意的是,MMU(一頁)的最小單位通常是4KB。這樣,每個記憶體對映需要關聯一個4KB的記憶體空間。4KB 大小可能會導致另一個問題,即整個頁表可能非常大。例如,僅 32 位系統就需要超過 1M 的頁表條目 (4GB/4KB)。為了解決頁表項過多的問題,Linux提供了兩種機制:多級頁表和大頁。

多級頁表 vs 大頁

多級頁表就是把記憶體分成塊來管理,改變原來的對映關係到塊索引和塊內的偏移量。由於通常只使用了一小部分虛擬記憶體空間,因此多級頁表只儲存了這些正在使用的塊,可以大大減少頁表項的數量。

Linux 使用四級頁表來管理記憶體頁。如下圖所示,虛擬地址分為5部分,前4個條目用於選擇頁面,最後一個索引代表頁面內的偏移量。

3.png
看大頁,顧名思義,就是比普通頁更大的記憶體塊,常見的大小有2MB和1GB。大頁通常用在使用大量記憶體的程序中,例如 Oracle、DPDK 等。

虛擬記憶體空間分佈

首先,我們需要了解更多關於虛擬記憶體空間的分佈情況。以 32 位系統為例,關係如下:

4.png

  • 只讀段:包括程式碼和常量等
  • 資料段:包括全域性變數等
  • 堆:包括動態分配的記憶體,從低地址開始向上增長
    對映區域:包括動態庫、共享庫等。從高地址開始向下增長。
  • 堆疊:包括區域性變數和函式呼叫的上下文等。堆疊大小固定為8MB。

在五個記憶體段中,堆和對映區域段的記憶體是動態分配的。例如,使用 C 標準庫 malloc() 或 mmap(),可以分別在堆和檔案對映段中動態分配記憶體。

記憶體分配和回收

malloc()是C標準庫提供的記憶體分配函式,對應系統呼叫,有兩種實現,分別是brk()和mmap()。

  • brk():小塊記憶體(≤128K),移動堆頂。brk()分配的記憶體不返回給系統,提高了記憶體訪問效率。
  • mmap():在對映區域分配記憶體。分配的記憶體在釋放時直接返回給系統。

要記住的一件事是,當呼叫這兩個函式時,實際上並沒有分配記憶體。記憶體只在第一次訪問時分配,即程序透過缺頁異常進入核心,然後核心分配記憶體。

當發現記憶體不足時,系統會透過一系列機制回收記憶體,例如以下三種方式:

  1. 回收快取,比如使用LRU(Least Recent Used)演算法回收最近最少使用的記憶體頁;
  2. 回收不經常訪問的記憶體,將不經常使用的記憶體透過交換分割槽直接寫入磁碟;
  3. 殺死程序。當記憶體緊張時,系統會透過OOM(Out of Memory)直接殺死佔用大量記憶體的程序。

OOM 用於oom_score對每個程序的記憶體使用情況進行評分:

  • 程序消耗的記憶體越大,oom_score
  • 程序執行所需的 CPU 越多,程序越小oom_score

當然,針對實際工作需要,管理員可以 oom_adj透過/proc檔案系統手動設定程序的程序,從而調整oom_score程序的程序。

範圍oom_adj是[-17, 15],值越大越容易被OOM殺死;該值越小,程序被OOM殺死的可能性越小,其中-17表示禁止OOM。我們來看看sshd過程:

$ ps -ef | grep sshd 
root 3218 1 0 2021?00:02:25 /usr/**in/sshd -D 
root 31328 3218 0 20:12?00:00:00 sshd:ubuntu [priv] 
ubuntu 31461 31328 0 20:12?00:00:00 sshd:ubuntu@pts/0 
ubuntu 31497 31462 0 20:13 pts/0 00:00:00 grep --color=auto sshd
$ cat /proc/3218/oom_adj 
-17

如你所見,程序的oom_adj設定為sshd-17。

如何檢查記憶體使用情況
你可以使用free,top或pscommand 來檢查系統記憶體使用情況。free顯示整個系統的記憶體使用情況。如果要檢查程序的記憶體使用情況,可以使用top或ps。

free

$ free -h 
              total used free shared buff/cache available 
Mem: 1.9G 1.0G 81M 2.6M 796M 715M 
Swap: 0B 0B 0B
  • 第一列,total是總記憶體大小
  • 第二列,used 是已用記憶體的大小,包括共享記憶體
  • 第三列,free是未使用記憶體的大小
  • 第四列,shared是共享記憶體的大小
  • 第五列,buff/cache是​​cache和buffer的大小
  • 最後一列可用,是新程序可用的記憶體量

top

# 按 M 按記憶體排序
$ top 
... 
KiB Mem : 總共 8169348, 6871440 免費, 267096 已使用, 1030812 buff/cache 
KiB Swap: 總共 0, 0 免費, 0已使用。7607492 利用 Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 
  430 root 19 -1 122360 35588 23748 S 0.0 0.4 0:32.17 systemd-journal 
1075 root 20 0 771860 22744 11368 S 0.0 010948 0:38.80 
snapd 1 17292 9488 S 0.0 0.2 0:00.24 networkd-dispat 
    1 root 20 0 78020 9156 6644 S 0.0 0.1 0:22.92 systemd 
12376 azure 20 0 76632 7456 6420 S 0.0 0.1 0:00.01 systemd 
12374 root 20 0 107984 7312 6304 S 0.0 0.1 0 :00.00 sshd 
...

頂部的輸出介面還顯示了系統的整體記憶體使用情況。這些資料與free類似,不再贅述。我們來看下面的內容,幾列與記憶體相關的資料,如VIRT、RES、SHR、%MEM。

  • VIRT是程序的虛擬記憶體大小。只要是程序申請的記憶體,即使實體記憶體沒有實際分配,也會被計算在內。
  • RES是常駐記憶體的大小,即程序實際使用的實體記憶體的大小,不包括Swap和共享記憶體。
  • SHR是共享記憶體的大小,比如其他程序使用的共享記憶體、載入的動態連結庫、程式程式碼段等。
  • %MEM是程序使用的實體記憶體佔系統總記憶體的百分比。

結論

對於普通程序,我們能看到的是核心提供的虛擬記憶體。這些虛擬記憶體也需要系統透過頁表對映到實體記憶體。

當一個程序透過malloc()申請記憶體時,記憶體不是立即分配的,而是在第一次訪問時透過缺頁異常在核心中分配記憶體。

由於程序的虛擬地址空間遠大於實體記憶體,Linux還提供了一系列機制來處理記憶體不足,如快取回收、交換分割槽、OOM等。

相關文章