Linux記憶體是怎麼工作的?
目錄
1 記憶體對映
說到記憶體,你能說出你現在用的這臺計算機記憶體有多大嗎?我估計你記得很清楚,因為這是我們購買時,首先考慮的一個重要引數,比方說,我的膝上型電腦記憶體就是 8GB 的 。
我們通常所說的記憶體容量,就像我剛剛提到的 8GB,其實指的是實體記憶體。實體記憶體也稱為主存,大多數計算機用的主存都是動態隨機訪問記憶體(DRAM)。只有核心才可以直接訪問實體記憶體。那麼,程式要訪問記憶體時,該怎麼辦呢?
Linux 核心給每個程式都提供了一個獨立的虛擬地址空間,並且這個地址空間是連續的。這樣,程式就可以很方便地訪問記憶體,更確切地說是訪問虛擬記憶體。
虛擬地址空間的內部又被分為核心空間和使用者空間兩部分,不同字長(也就是單個 CPU 指令可以處理資料的最大長度)的處理器,地址空間的範圍也不同。比如最常見的 32 位和 64 位系統,我畫了兩張圖來分別表示它們的虛擬地址空間,如下所示:
通過這裡可以看出,32 位系統的核心空間佔用 1G,位於最高處,剩下的 3G 是使用者空間。而 64 位系統的核心空間和使用者空間都是 128T,分別佔據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。
還記得程式的使用者態和核心態嗎?程式在使用者態時,只能訪問使用者空間記憶體;只有進入核心態後,才可以訪問核心空間記憶體。雖然每個程式的地址空間都包含了核心空間,但這些核心空間,其實關聯的都是相同的實體記憶體。這樣,程式切換到核心態後,就可以很方便地訪問核心空間記憶體。
既然每個程式都有一個這麼大的地址空間,那麼所有程式的虛擬記憶體加起來,自然要比實際的實體記憶體大得多。所以,並不是所有的虛擬記憶體都會分配實體記憶體,只有那些實際使用的虛擬記憶體才分配實體記憶體,並且分配後的實體記憶體,是通過記憶體對映來管理的。
記憶體對映,其實就是將虛擬記憶體地址對映到實體記憶體地址。為了完成記憶體對映,核心為每個程式都維護了一張頁表,記錄虛擬地址與實體地址的對映關係,如下圖所示:
頁表實際上儲存在 CPU 的記憶體管理單元 MMU 中,這樣,正常情況下,處理器就可以直接通過硬體,找出要訪問的記憶體。
而當程式訪問的虛擬地址在頁表中查不到時,系統會產生一個缺頁異常,進入核心空間分配實體記憶體、更新程式頁表,最後再返回使用者空間,恢復程式的執行。
另外,在 CPU 上下文切換的文章中曾經提到, TLB(Translation Lookaside Buffer,轉譯後備緩衝器)會影響 CPU 的記憶體訪問效能,在這裡其實就可以得到解釋。
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 直接相加得出結果。
5 小結
對普通程式來說,它能看到的其實是核心提供的虛擬記憶體,這些虛擬記憶體還需要通過頁表,由系統對映為實體記憶體。
當程式通過 malloc() 申請記憶體後,記憶體並不會立即分配,而是在首次訪問時,才通過缺頁異常陷入核心中分配記憶體。由於程式的虛擬地址空間比實體記憶體大很多,Linux 還提供了一系列的機制,應對記憶體不足的問題,比如快取的回收、交換分割槽 Swap 以及 OOM 等。
當你需要了解系統或者程式的記憶體使用情況時,可以用 free 和 top 、ps 等效能工具。它們都是分析效能問題時最常用的效能工具,希望你能熟練使用它們,並真正理解各個指標的含義。
相關文章
- 在Linux中,什麼是虛擬記憶體?它是如何工作的?Linux記憶體
- 《Linux是怎麼樣工作的》讀書筆記Linux筆記
- C++ 類的記憶體分配是怎麼樣的?C++記憶體
- 什麼是記憶體記憶體
- 效能測試必備知識(10)- Linux 是怎麼管理記憶體的?Linux記憶體
- JavaScript 是如何工作的:JavaScript 的記憶體模型JavaScript記憶體模型
- AntDB記憶體管理之記憶體上下文之記憶體上下文機制是怎麼實現的記憶體
- 什麼是Java記憶體模型(JMM)中的主記憶體和本地記憶體?Java記憶體模型
- 想知道記憶體條的工作原理?要怎麼選購DDR記憶體條?宏旺半導體科普記憶體
- Linux 中的“大記憶體頁”(hugepage)是個什麼?Linux記憶體
- 在Linux中,記憶體怎麼看?磁碟狀態怎麼看?Linux記憶體
- win10工作管理員怎麼檢視記憶體使用Win10記憶體
- Java記憶體模型FAQ(一) 什麼是記憶體模型Java記憶體模型
- 什麼是Java記憶體模型?Java記憶體模型
- 什麼是Java記憶體模型Java記憶體模型
- 顯示卡的視訊記憶體是什麼?記憶體
- JavaScript 是如何工作的:記憶體管理 + 如何處理四種常見的記憶體洩漏JavaScript記憶體
- win10記憶體不足怎麼解決_win10記憶體不足怎麼辦Win10記憶體
- 記憶體資料庫的行存表索引是怎麼做到加速的記憶體資料庫索引
- Python如何管理記憶體?記憶體分配機制是什麼?Python記憶體
- 虛擬記憶體系統——瞭解記憶體的工作原理記憶體
- pycharm提示記憶體不足怎麼辦PyCharm記憶體
- mongodb記憶體不足怎麼解決?MongoDB記憶體
- 虛擬記憶體有什麼用 虛擬記憶體不足怎麼解決記憶體
- win10為硬體保留的記憶體怎麼釋放 win10取消為硬體保留的記憶體怎麼操作Win10記憶體
- ssl/tls是什麼?是怎麼工作的?TLS
- Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題?Java記憶體模型
- win10怎麼調整虛擬記憶體_win10怎麼調虛擬記憶體Win10記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- Java記憶體模型FAQ(九)在新的Java記憶體模型中,final欄位是如何工作的Java記憶體模型
- [譯] JavaScript是如何工作的:記憶體管理 + 如何處理4個常見的記憶體洩漏(譯)JavaScript記憶體
- win10工作管理員記憶體佔用過高怎麼解決Win10記憶體
- 教你如何擴大電腦的虛擬記憶體? 什麼是虛擬記憶體?記憶體
- Linux 記憶體管理:記憶體對映Linux記憶體
- w10老顯示記憶體不足怎麼解決 w10記憶體總是顯示記憶體不足處理方法記憶體
- win10虛擬記憶體怎麼設定 筆記本win10虛擬記憶體怎麼看Win10記憶體筆記
- mysql order by是怎麼工作的?MySql
- Mybatis是怎麼工作的(二)MyBatis