1.Linux內存是怎么工作的
1.內存映射:
說到內存,你能說出你現在用的這台計算機內存有多大嗎?我估計你記得很清楚,因為這是我們購買時,首先考慮的一個重要參數,比方說,我的筆記本電腦內存就是 8GB 的 。
我們通常所說的內存容量,就像我剛剛提到的 8GB,其實指的是物理內存。物理內存也稱為主存,大多數計算機用的主存都是動態隨機訪問內存(DRAM)。只有內核才可以直接訪問物理內存。那么,進程要訪問內存時,該怎么辦呢?
Linux 內核給每個進程都提供了一個獨立的虛擬地址空間,並且這個地址空間是連續的。這樣,進程就可以很方便地訪問內存,更確切地說是訪問虛擬內存。
虛擬地址空間的內部又被分為內核空間和用戶空間兩部分,不同字長(也就是單個 CPU 指令可以處理數據的最大長度)的處理器,地址空間的范圍也不同。比如最常見的 32 位和 64 位系統,我畫了兩張圖來分別表示它們的虛擬地址空間,如下所示:
通過這里可以看出,32 位系統的內核空間占用 1G,位於最高處,剩下的 3G 是用戶空間。而 64 位系統的內核空間和用戶空間都是 128T,分別占據整個內存空間的最高和最低處,剩下的中間部分是未定義的。
還記得進程的用戶態和內核態嗎?進程在用戶態時,只能訪問用戶空間內存;只有進入內核態后,才可以訪問內核空間內存。雖然每個進程的地址空間都包含了內核空間,但這些內核空間,其實關聯的都是相同的物理內存。這樣,進程切換到內核態后,就可以很方便地訪問內核空間內存。
既然每個進程都有一個這么大的地址空間,那么所有進程的虛擬內存加起來,自然要比實際的物理內存大得多。所以,並不是所有的虛擬內存都會分配物理內存,只有那些實際使用的虛擬內存才分配物理內存,並且分配后的物理內存,是通過內存映射來管理的。
內存映射,其實就是將虛擬內存地址映射到物理內存地址。為了完成內存映射,內核為每個進程都維護了一張頁表,記錄虛擬地址與物理地址的映射關系,如下圖所示:
頁表實際上存儲在 CPU 的內存管理單元 MMU 中,這樣,正常情況下,處理器就可以直接通過硬件,找出要訪問的內存。而當進程訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入內核空間分配物理內存、更新進程頁表,最后再返回用戶空間,恢復進程的運行。
TLB 其實就是 MMU 中頁表的高速緩存。由於進程的虛擬地址空間是獨立的,而 TLB 的訪問速度又比 MMU 快得多,所以,通過減少進程的上下文切換,減少 TLB 的刷新次數,就可以提高 TLB 緩存的使用率,進而提高 CPU 的內存訪問性能。
不過要注意,MMU 並不以字節為單位來管理內存,而是規定了一個內存映射的最小單位,也就是頁,通常是 4 KB 大小。這樣,每一次內存映射,都需要關聯 4 KB 或者 4KB 整數倍的內存空間。
頁的大小只有 4 KB ,導致的另一個問題就是,整個頁表會變得非常大。比方說,僅 32 位系統就需要 100 多萬個頁表項(4GB/4KB),才可以實現整個地址空間的映射。為了解決頁表項過多的問題,Linux 提供了兩種機制,也就是多級頁表和大頁(HugePage)。
多級頁表就是把內存分成區塊來管理,將原來的映射關系改成區塊索引和區塊內的偏移。由於虛擬內存空間通常只用了很少一部分,那么,多級頁表就只保存這些使用中的區塊,這樣就可以大大地減少頁表的項數。
Linux 用的正是四級頁表來管理內存頁,如下圖所示,虛擬地址被分為 5 個部分,前 4 個表項用於選擇頁,而最后一個索引表示頁內偏移。
再看大頁,顧名思義,就是比普通頁更大的內存塊,常見的大小有 2MB 和 1GB。大頁通常用在使用大量內存的進程上,比如 Oracle、DPDK 等。
通過這些機制,在頁表的映射下,進程就可以通過虛擬地址來訪問物理內存了。那么具體到一個 Linux 進程中,這些內存又是怎么使用的呢?
2.虛擬內存空間分布
首先,我們需要進一步了解虛擬內存空間的分布情況。最上方的內核空間不用多講,下方的用戶空間內存,其實又被分成了多個不同的段。以 32 位系統為例,我畫了一張圖來表示它們的關系。
通過這張圖你可以看到,用戶空間內存,從低到高分別是五種不同的內存段。
- 只讀段,包括代碼和常量等。
- 數據段,包括全局變量等。
- 堆,包括動態分配的內存,從低地址開始向上增長。
- 文件映射段,包括動態庫、共享內存等,從高地址開始向下增長。
-
棧,包括局部變量和函數調用的上下文等。棧的大小是固定的,一般是 8 MB。
在這五個內存段中,堆和文件映射段的內存是動態分配的。比如說,使用 C 標准庫的 malloc() 或者 mmap() ,就可以分別在堆和文件映射段動態分配內存。
其實 64 位系統的內存分布也類似,只不過內存空間要大得多。那么,更重要的問題來了,內存究竟是怎么分配的呢?
3.內存分配與回收
malloc() 是 C 標准庫提供的內存分配函數,對應到系統調用上,有兩種實現方式,即 brk() 和 mmap()。
對小塊內存(小於 128K),C 標准庫使用 brk() 來分配,也就是通過移動堆頂的位置來分配內存。這些內存釋放后並不會立刻歸還系統,而是被緩存起來,這樣就可以重復使用。
而大塊內存(大於 128K),則直接使用內存映射 mmap() 來分配,也就是在文件映射段找一塊空閑內存分配出去。
這兩種方式,自然各有優缺點。
brk() 方式的緩存,可以減少缺頁異常的發生,提高內存訪問效率。不過,由於這些內存沒有歸還系統,在內存工作繁忙時,頻繁的內存分配和釋放會造成內存碎片。
而 mmap() 方式分配的內存,會在釋放時直接歸還系統,所以每次 mmap 都會發生缺頁異常。在內存工作繁忙時,頻繁的內存分配會導致大量的缺頁異常,使內核的管理負擔增大。這也是 malloc 只對大塊內存使用 mmap 的原因。
了解這兩種調用方式后,我們還需要清楚一點,那就是,當這兩種調用發生后,其實並沒有真正分配內存。這些內存,都只在首次訪問時才分配,也就是通過缺頁異常進入內核中,再由內核來分配內存。
整體來說,Linux 使用伙伴系統來管理內存分配。前面我們提到過,這些內存在 MMU 中以頁為單位進行管理,伙伴系統也一樣,以頁為單位來管理內存,並且會通過相鄰頁的合並,減少內存碎片化(比如 brk 方式造成的內存碎片)。
你可能會想到一個問題,如果遇到比頁更小的對象,比如不到 1K 的時候,該怎么分配內存呢?
實際系統運行中,確實有大量比頁還小的對象,如果為它們也分配單獨的頁,那就太浪費內存了。
所以,在用戶空間,malloc 通過 brk() 分配的內存,在釋放時並不立即歸還系統,而是緩存起來重復利用。在內核空間,Linux 則通過 slab 分配器來管理小內存。你可以把 slab 看成構建在伙伴系統上的一個緩存,主要作用就是分配並釋放內核中的小對象。
對內存來說,如果只分配而不釋放,就會造成內存泄漏,甚至會耗盡系統內存。所以,在應用程序用完內存后,還需要調用 free() 或 unmap() ,來釋放這些不用的內存。
當然,系統也不會任由某個進程用完所有內存。在發現內存緊張時,系統就會通過一系列機制來回收內存,比如下面這三種方式:
- 回收緩存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的內存頁面;
- 回收不常訪問的內存,把不常用的內存通過交換分區直接寫到磁盤中;
- 殺死進程,內存緊張時系統還會通過 OOM(Out of Memory),直接殺掉占用大量內存的進程。
其中,第二種方式回收不常訪問的內存時,會用到交換分區(以下簡稱 Swap)。Swap 其實就是把一塊磁盤空間當成內存來用。它可以把進程暫時不用的數據存儲到磁盤中(這個過程稱為換出),當進程訪問這些內存時,再從磁盤讀取這些數據到內存中(這個過程稱為換入)。
所以,你可以發現,Swap 把系統的可用內存變大了。不過要注意,通常只在內存不足時,才會發生 Swap 交換。並且由於磁盤讀寫的速度遠比內存慢,Swap 會導致嚴重的內存性能問題。
第三種方式提到的 OOM(Out of Memory),其實是內核的一種保護機制。它監控進程的內存使用情況,並且使用 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.如何查看內存使用情況
通過了解內存空間的分布,以及內存的分配和回收,我想你對內存的工作原理應該有了大概的認識。當然,系統的實際工作原理更加復雜,也會涉及其他一些機制,這里我只講了最主要的原理。掌握了這些,你可以對內存的運作有一條主線認識,不至於腦海里只有術語名詞的堆砌。
那么在了解內存的工作原理之后,我們又該怎么查看系統內存使用情況呢?
其實前面 CPU 內容的學習中,我們也提到過一些相關工具。在這里,你第一個想到的應該是 free 工具吧。下面是一個 free 的輸出示例:
# 注意不同版本的 free 輸出可能會有所不同
$ free
total used free shared buff/cache available
Mem:
8169348
263524
6875352
668
1030472
7611064
Swap:
0
0
0
|
你可以看到,free 輸出的是一個表格,其中的數值都默認以字節為單位。表格總共有兩行六列,這兩行分別是物理內存 Mem 和交換分區 Swap 的使用情況,而六列中,每列數據的含義分別為:
- 第一列,total 是總內存大小;
- 第二列,used 是已使用內存的大小,包含了共享內存;
- 第三列,free 是未使用內存的大小;
- 第四列,shared 是共享內存的大小;
- 第五列,buff/cache 是緩存和緩沖區的大小;
- 最后一列,available 是新進程可用內存的大小。
這里尤其注意一下,最后一列的可用內存 available 。available 不僅包含未使用內存,還包括了可回收的緩存,所以一般會比未使用內存更大。不過,並不是所有緩存都可以回收,因為有些緩存可能正在使用中。
不過,我們知道,free 顯示的是整個系統的內存使用情況。如果你想查看進程的內存使用情況,可以用 top 或者 ps 等工具。比如,下面是 top 的輸出示例:
# 按下 M 切換到內存排序
$ top
...
KiB Mem :
8169348
total,
6871440
free,
267096
used,
1030812
buff/cache
KiB Swap:
0
total,
0
free,
0
used.
7607492
avail 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
0.3
0
:
38.89
snapd
1048
root
20
0
170904
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
...
|
top 輸出界面的頂端,也顯示了系統整體的內存使用情況,這些數據跟 free 類似,我就不再重復解釋。我們接着看下面的內容,跟內存相關的幾列數據,比如 VIRT、RES、SHR 以及 %MEM 等。
這些數據,包含了進程最重要的幾個內存使用情況,我們挨個來看。
- VIRT 是進程虛擬內存的大小,只要是進程申請過的內存,即便還沒有真正分配物理內存,也會計算在內。
- RES 是常駐內存的大小,也就是進程實際使用的物理內存大小,但不包括 Swap 和共享內存。
- SHR 是共享內存的大小,比如與其他進程共同使用的共享內存、加載的動態鏈接庫以及程序的代碼段等。
- %MEM 是進程使用物理內存占系統總內存的百分比。
除了要認識這些基本信息,在查看 top 輸出時,你還要注意兩點。
第一,虛擬內存通常並不會全部分配物理內存。從上面的輸出,你可以發現每個進程的虛擬內存都比常駐內存大得多。
第二,共享內存 SHR 並不一定是共享的,比方說,程序的代碼段、非共享的動態鏈接庫,也都算在 SHR 里。當然,SHR 也包括了進程間真正共享的內存。所以在計算多個進程的內存使用時,不要把所有進程的 SHR 直接相加得出結果。
小結
我們梳理了 Linux 內存的工作原理。對普通進程來說,它能看到的其實是內核提供的虛擬內存,這些虛擬內存還需要通過頁表,由系統映射為物理內存。
當進程通過 malloc() 申請內存后,內存並不會立即分配,而是在首次訪問時,才通過缺頁異常陷入內核中分配內存。
由於進程的虛擬地址空間比物理內存大很多,Linux 還提供了一系列的機制,應對內存不足的問題,比如緩存的回收、交換分區 Swap 以及 OOM 等。
當你需要了解系統或者進程的內存使用情況時,可以用 free 和 top 、ps 等性能工具。它們都是分析性能問題時最常用的性能工具。
2.理解內存中的Buffer和Cache?
注:今天內容接下來的部分,Buffer 和 Cache 我會都用英文來表示,避免跟文中的“緩存”一詞混淆。而文中的“緩存”,則通指內存中的臨時存儲。
1.free 數據的來源
在正式講解兩個概念前,你可以先想想,你有沒有什么途徑來進一步了解它們?除了中文翻譯直接得到概念,別忘了,Buffer 和 Cache 還是我們用 free 獲得的指標。
碰到看不明白的指標時該怎么辦?用 man 命令查詢 free 的文檔,就可以找到對應指標的詳細說明。比如,我們執行 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
|
從 free 的手冊中,你可以看到 buffer 和 cache 的說明。
- Buffers 是內核緩沖區用到的內存,對應的是 /proc/meminfo 中的 Buffers 值。
- Cache 是內核頁緩存和 Slab 用到的內存,對應的是 /proc/meminfo 中的 Cached 與 SReclaimable 之和。
這里的說明告訴我們,這些數值都來自 /proc/meminfo,但更具體的 Buffers、Cached 和 SReclaimable 的含義,還是沒有說清楚。
2.proc 文件系統
在前面 CPU 性能模塊就曾經提到過,/proc 是 Linux 內核提供的一種特殊文件系統,是用戶跟內核交互的接口。比方說,用戶可以從 /proc 中查詢內核的運行狀態和配置選項,查詢進程的運行狀態、統計數據等,當然,你也可以通過 /proc 來修改內核的配置。
proc 文件系統同時也是很多性能工具的最終數據來源。比如我們剛才看到的 free ,就是通過讀取 /proc/meminfo ,得到內存的使用情況。
繼續說回 /proc/meminfo,既然 Buffers、Cached、SReclaimable 這幾個指標不容易理解,那我們還得繼續查 proc 文件系統,獲取它們的詳細定義。
執行 man proc ,你就可以得到 proc 文件系統的詳細文檔。
注意這個文檔比較長,你最好搜索一下(比如搜索 meminfo),以便更快定位到內存部分。
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 記錄。
好了,我們終於找到了這三個指標的詳細定義。
3.小結
我們一起探索了內存性能中 Buffer 和 Cache 的詳細含義。Buffer 和 Cache 分別緩存磁盤和文件系統的讀寫數據。
- 從寫的角度來說,不僅可以優化磁盤和文件的寫入,對應用程序也有好處,應用程序可以在數據真正落盤前,就返回去做其他工作。
- 從讀的角度來說,既可以加速讀取那些需要頻繁訪問的數據,也降低了頻繁 I/O 對磁盤的壓力。