一、Linux記憶體工作原理
1,記憶體對映
Linux核心給每個程式都提供了一個獨立的虛擬空間,並且這個地址空間是連續的。這樣,程式就可以很方便地訪問記憶體,更確切地說是訪問虛擬記憶體。
虛擬地址空間的內部又被分為核心空間和使用者空間兩部分,不同字長(也就是單個CPU指令可以處理資料的最大長度)的處理器,地址空間的範圍也不同。比如常見的32位和64位系統
- 32位系統的核心空間佔用1G,位於最高處,剩下的3G是使用者空間
- 64位系統的核心空間和使用者空間都是128T,分別佔據整個記憶體空間的最高處和最低處,剩下的中間部分是未定義的。
程式在使用者態時,只能訪問使用者空間記憶體;只有進入核心態後,才可以訪問核心空間記憶體。雖然每個程式的地址空間都包含了核心空間,但這些核心空間其實關聯的都是相同的實體記憶體。這樣,程式切換到核心態後,就可以很方便地訪問核心空間記憶體。既然每個程式都有一個這麼大的地址空間,那麼所有的虛擬記憶體加起來,自然要比實際的實體記憶體大很多。所以,並不是所有的虛擬記憶體都會分配實體記憶體,只有那些實際使用的虛擬記憶體才分配實體記憶體,並且分配後的實體記憶體,是通過記憶體對映來管理的。
記憶體對映,其實就是將虛擬記憶體地址對映到實體記憶體地址。為了完成記憶體對映,核心為每個程式都維護了一張頁表,記錄虛擬地址與實體地址的對映關係:
頁表實際上儲存在CPU的記憶體管理單元MMU中,正常情況下,處理器就可以直接通過硬體找出訪問的記憶體。而當程式訪問的記憶體地址在頁表中查不到時,系統會產生一個缺頁異常,進入核心空間分配實體記憶體、更新程式頁表,最後在返回使用者空間,恢復程式的執行。
TLB(Translation Lookaside Buffer,後備緩衝器)會影響CPU的記憶體訪問效能:TLB其實就是MMU中頁表的快取記憶體。由於程式的虛擬地址空間是獨立的,而TLB的訪問速度又比MMU快很多,所以,通過減少上下文切換,減少TLB的重新整理次數,就可以提高TLB快取的使用率,進而提高CPU的記憶體訪問效能。
MMU規定了一個記憶體對映的最小單位,也就是頁,通常是4KB。每次記憶體對映,都需要關聯4KB或者4KB整數倍的記憶體空間。由於頁的大小隻有4KB,導致的整個頁表會變的非常大。例如:32位系統就需要100多萬個頁表項(4GB/4KB),才可以實現整個地址空間的對映。為了解決頁表項過多的問題,Linux提供了兩種機制:多級頁表 和 大頁(HugePage)
多級頁表:把整個記憶體分成區塊來管理,將原來的對映關係改成區塊索引和區塊內的偏移。由於虛擬記憶體空間通常只用了很少一部分,那麼多級頁表就只儲存這些使用中的區塊,這樣就可以大大減少頁表的選項。Linux用的正是四級頁表來管理記憶體頁,如圖,虛擬地址被分為5個部分,前4個表項用於選擇頁,最後一個索引表示頁內偏移。
大頁:顧名思義就是比普通也更大的記憶體塊,常見的大小有2MB和1GB。大頁通常用在使用大量記憶體的程式上,比如Oracle、DPDK等。
2,虛擬記憶體空間分佈
粗略繪製32位系統的虛擬記憶體空間分佈圖
從此圖可以看出,使用者空間記憶體從低到高分別是五種不同的記憶體段。
- 只讀段,包括程式碼和常量等
- 資料段,包括全域性變數等
- 堆,包括動態分配的記憶體,從低地址開始向上增長
- 檔案對映段,包括動態庫、共享空間,從高地址開始向下增長
- 棧,包括區域性變數和函式呼叫的上下文等。棧的大小固定,一般為8MB
在這五個記憶體段中,堆和檔案對映段的記憶體是動態分配的。比如說,使用C標準庫的malloc()或mmap(),就可以分別在堆和檔案對映段動態分配記憶體。
3,記憶體分配與回收
a>記憶體分配
malloc()是C標準庫提供的記憶體分配函式,對應到系統呼叫上,有兩種實現方式,即brk()和mmap()。
- 對小塊記憶體(小於128K),C標準庫使用brk()來分配,也就是通過移動堆頂的位置來分配記憶體。這些記憶體釋放後並不會立即歸還系統,而是被快取起來,這樣就可以重複使用。
- brk()方式的快取,可以減少缺頁異常的發生,提高記憶體訪問效率。不過由於這些記憶體沒有歸還系統,在記憶體工作繁忙是,頻繁的記憶體分配和釋放會造成記憶體碎片
- 對大塊記憶體(大於128K),則直接使用記憶體對映mmap()來分配,也就是在檔案對映段找一塊空閒記憶體分配出去。
- mmap()方式分配的記憶體,會在釋放時直接歸還系統,所以每次mmap都會發生缺頁異常。在記憶體工作繁忙時,頻繁的記憶體分配會造成大量的缺頁異常,使核心的管理負擔增大。這也是malloc只對大塊記憶體使用mmap的原因
當這兩種呼叫發生後,其實並沒有真正分配記憶體。這些記憶體都只在首次訪問時才分配,也就是通過缺頁異常進入核心中,再由核心來分配記憶體。Linux使用夥伴系統來管理記憶體分配,與MMU的頁管理一樣,夥伴系統也是以頁為單位來管理記憶體的,並通過相鄰頁的合併,較少記憶體碎片化(比如brk方式造成的記憶體碎片)。
b>記憶體回收
對於記憶體來說,如果只分配而不釋放,就會造成記憶體洩露,甚至會耗盡系統記憶體。所以,在應用程式用完記憶體後,需要呼叫 free() 或 unmap() ,來釋放不用的記憶體。在發現記憶體緊張時,系統就會通過一系列機制來回收記憶體:
- 回收快取,比如使用 LRU(Least Recently Used)演算法,回收最近最少使用的記憶體頁面
- 回收不常訪問的記憶體,把不常用的記憶體通過交換分割槽直接寫到磁碟中
- 殺死程式,記憶體緊張時系統會通過 OOM(Out Of Memory),直接殺掉佔用大量記憶體的程式
其中,第二種方式回收不常訪問的記憶體時,會用到交換分割槽(Swap)。Swap其實就是吧一塊磁碟空間當成記憶體來用。它可以吧程式暫時不用的資料儲存到磁碟中(這個過程稱為換出),當程式訪問這些記憶體時,在從磁碟讀取這些資料到記憶體(這個過程稱為換入)。所以Swap把系統的可用記憶體變大了。不過通常只在記憶體不足時,才會發生Swap交換,並且由於磁碟讀寫的速度遠比記憶體慢,Swap會導致嚴重的記憶體效能問題。
第三種方式提到的OOM,其實時核心的一種保護機制。它監控程式的記憶體使用情況,並且使用 oom_score 為每個程式的記憶體使用情況進行評分:
- 一個程式消耗的記憶體越大,oom_score就越大
- 一個程式執行佔用的CPU越多,oom_score就越小
程式的oom_score越大,代表消耗的記憶體越多,也就越容易被OOM殺死,從而可以更好保護系統。結合實際需求,可以通過 /proc 檔案系統,手動設定程式的 oom_adj,從而調整oom_score。oom_adj的範圍是[-17,15],數值越大表示程式越容易被OOM殺死;數值越小表示程式越不容易被OOM殺死,其中 -17 表示禁止OOM。例如:手動調整sshd程式的oom_adj 為 -16,保障sshd程式不容易被OOM殺死
echo -16 > /proc/$(pidof sshd)/oom_adj
4,檢視記憶體使用情況
a>free
- total:總記憶體大小
- used:已使用記憶體大小,包含了共享記憶體
- free:未使用記憶體大小
- shared:共享記憶體大小
- buff/cache:快取和緩衝區大小
- available:新程式可用記憶體大小。不僅包含未使用記憶體,還包括了可回收的記憶體,一般比未使用記憶體更大(但並不是所有快取都可以回收,因為有的快取可能正在使用中)
b>top
- VIRT:程式的虛擬記憶體大小,只要是程式申請過的記憶體,即便還沒有真正分配實體記憶體,也會計算在內。
- RES:常駐記憶體的大小,也就是進程實際使用的實體記憶體大小,但不包括Swap和共享記憶體。
- SHR:共享記憶體的大小,比如與其他程式共同使用的共享記憶體、載入的動態連結庫以及程式的程式碼段等。
- %MEM:程式使用實體記憶體佔系統總記憶體的百分比
二、Buffer/Cache
1,定義
使用man free檢視
buffers Memory used by kernel buffers (Buffers in /proc/meminfo) cache Memory used by the page cache and slabs (Cached and SReclaimable in /proc/meminfo) buff/cache Sum of buffers and cache
- Buffers 是核心緩衝區用到的記憶體,對應的是 /proc/meminfo 中的Buffers值
- Cache 是核心頁快取和 Slab 用到的記憶體,對應的是 /proc/meminfo 中的Cached 與 SReclaimable之和
使用man proc 檢視
Buffers %lu Relatively temporary storage for raw disk blocks that shouldn't get tremendously large (20MB or so). Cached %lu In-memory cache for files read from the disk (the page cache). Doesn't include SwapCached. SReclaimable %lu (since Linux 2.6.19) Part of Slab, that might be reclaimed, such as caches. SUnreclaim %lu (since Linux 2.6.19) Part of Slab, that cannot be reclaimed on memory pressure.
- Buffers 是對原始磁碟塊的臨時儲存,也就是用來快取磁碟的資料,通常不會特別大(20MB左右)。這樣,核心就可以把分散的寫集中起來,統一優化磁碟的寫入,比如可以把多次小的寫合併成單次大的寫等。
- Cached 是從磁碟讀取檔案的頁快取,也就是用來快取從檔案讀取的資料。這樣,下次訪問這些檔案資料時,就可以直接從記憶體中快速獲取,而不需要再次訪問緩慢的磁碟。
- SReclaimable 是Slab 的一部分。Slab 包括兩個部分,其中的可回收部分用 SReclaimable 記錄;不可回收部分用 SUnreclaim 記錄
2,案例
清理系統快取
#清理檔案頁、目錄項、Inodes 等各種快取 echo 3 > /proc/sys/vm/drop_caches
a>磁碟和檔案寫案例
終端1:首先輸出vmstat
- buff 和 cache就是前面說的Buffers 和 Cache,單位是KB
- bi 和 bo 則分別表示塊裝置讀取和寫入的大小,單位為 塊/秒。因為Linux中塊的大小是1KB,所以等價於KB/s
終端2:執行dd命令,通過讀取隨機裝置,生成一個500MB大小的檔案
dd if=/dev/urandom of=/tmp/file bs=1M count=500
終端1:繼續觀察vmstat中的Buffer 和 Cache。
發現在dd命令執行時,Cache 在不停地增長,而Buffer 基本保持不變:
- 在cache剛開始增長時,塊裝置I/O很少,bi值 只出現了一次 488KB/s,bo 則只有一次4KB,過一段時間後,才會出現大量的塊裝置寫,bo甚至高達12880 KB/s
- 當dd命令結束後,Cache不在增長,但是塊裝置寫還會持續一段時間,並且多次I/O寫的結果加起來,才是dd要寫的500M資料
終端2:清理快取後,向磁碟/dev/sdb1 寫入2GB的隨機資料
#清理檔案頁、目錄項、Inodes 等各種快取 echo 3 > /proc/sys/vm/drop_caches #執行dd 命令向磁碟分割槽 /dev/sdb1 寫入2G資料 dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048
終端1:觀察記憶體和I/O變化
此時可以看出,雖然都是寫資料,但是寫磁碟和寫檔案的現象不太一樣。寫磁碟時(也就是bo大於0時),Buffer和Cache都在增長,但是顯然Buffer增長的快很多。這說明,寫磁碟用到了大量的Buffer。
關於Cache,在寫檔案時會用到Cache快取資料,而寫磁碟則會用到Buffer快取資料。所以,Cache是檔案讀的快取,實際Cache也會快取寫檔案時的資料。
b>磁碟和檔案讀案例
終端2:從檔案/tmp/file中讀取資料寫入空裝置
#清理檔案頁、目錄項、Inodes 等各種快取 echo 3 > /proc/sys/vm/drop_caches #執行dd 命令讀取檔案資料 dd if=/tmp/file of=/dev/null
終端1:vmstat觀察記憶體和I/O變化情況
觀察vmstat輸出,發現讀取檔案時(bi大於)),Buffer保持不變,而Cache在不停增長。
終端2:清理快取,從磁碟分割槽 /dev/sda1中讀取資料,寫入空裝置
#清理檔案頁、目錄項、Inodes 等各種快取 echo 3 > /proc/sys/vm/drop_caches #執行dd 命令讀取檔案資料 dd if=/dev/sda1 of=/dev/null bs=1M count=1024
終端1:vmstat觀察記憶體和I/O變化情況
發現在讀磁碟時(bi大於0),Buffer和Cache都在增長,但顯然Buffer增長的快很多。說明讀磁碟時,資料快取到了Buffer中。
c>總結
- Buffer 既可以用作寫入磁碟資料的快取,也可以用作從磁碟讀取資料的快取
- Cache 既可以用作從檔案讀取資料的也快取,也可以用作寫檔案的頁快取
Buffer是對磁碟資料的快取,而Cache是檔案資料的快取,它們既會用在讀請求中,也會用在寫請求中。