作者:小林coding
計算機八股文刷題網站:https://xiaolincoding.com/
大家好,我是小林。
看到讀者在群裡討論這些面試題:
其中,第一個問題「在 4GB 實體記憶體的機器上,申請 8G 記憶體會怎麼樣?」存在比較大的爭議,有人說會申請失敗,有的人說可以申請成功。
這個問題在沒有前置條件下,就說出答案就是耍流氓。這個問題要考慮三個前置條件:
- 作業系統是 32 位的,還是 64 位的?
- 申請完 8G 記憶體後會不會被使用?
- 作業系統有沒有使用 Swap 機制?
所以,我們要分場景討論。
作業系統虛擬記憶體大小
應用程式通過 malloc 函式申請記憶體的時候,實際上申請的是虛擬記憶體,此時並不會分配實體記憶體。
當應用程式讀寫了這塊虛擬記憶體,CPU 就會去訪問這個虛擬記憶體, 這時會發現這個虛擬記憶體沒有對映到實體記憶體, CPU 就會產生缺頁中斷,程式會從使用者態切換到核心態,並將缺頁中斷交給核心的 Page Fault Handler (缺頁中斷函式)處理。
缺頁中斷處理函式會看是否有空閒的實體記憶體:
- 如果有,就直接分配實體記憶體,並建立虛擬記憶體與實體記憶體之間的對映關係。
- 如果沒有空閒的實體記憶體,那麼核心就會開始進行回收記憶體的工作,如果回收記憶體工作結束後,空閒的實體記憶體仍然無法滿足此次實體記憶體的申請,那麼核心就會放最後的大招了觸發 OOM (Out of Memory)機制。
32 位作業系統和 64 位作業系統的虛擬地址空間大小是不同的,在 Linux 作業系統中,虛擬地址空間的內部又被分為核心空間和使用者空間兩部分,如下所示:
通過這裡可以看出:
32
位系統的核心空間佔用1G
,位於最高處,剩下的3G
是使用者空間;64
位系統的核心空間和使用者空間都是128T
,分別佔據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。
現在可以回答這個問題了:在 32 位作業系統、4GB 實體記憶體的機器上,申請 8GB 記憶體,會怎麼樣?
因為 32 位作業系統,程式最多隻能申請 3 GB 大小的虛擬記憶體空間,所以程式申請 8GB 記憶體的話,在申請虛擬記憶體階段就會失敗(我手上沒有 32 位作業系統測試,我估計失敗的原因是 OOM)。
在 64 位作業系統、4GB 實體記憶體的機器上,申請 8G 記憶體,會怎麼樣?
64 位作業系統,程式可以使用 128 TB 大小的虛擬記憶體空間,所以程式申請 8GB 記憶體是沒問題的,因為程式申請記憶體是申請虛擬記憶體,只要不讀寫這個虛擬記憶體,作業系統就不會分配實體記憶體。
我們可以簡單做個測試,我的伺服器是 64 位作業系統,但是實體記憶體只有 2 GB:
現在,我在機器上,連續申請 4 次 1 GB 記憶體,也就是一共申請了 4 GB 記憶體,注意下面程式碼只是單純分配了虛擬記憶體,並沒有使用該虛擬記憶體:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MEM_SIZE 1024 * 1024 * 1024
int main() {
char* addr[4];
int i = 0;
for(i = 0; i < 4; ++i) {
addr[i] = (char*) malloc(MEM_SIZE);
if(!addr[i]) {
printf("執行 malloc 失敗, 錯誤:%s\n",strerror(errno));
return -1;
}
printf("主執行緒呼叫malloc後,申請1gb大小得記憶體,此記憶體起始地址:0X%x\n", addr[i]);
}
//輸入任意字元後,才結束
getchar();
return 0;
}
然後執行這個程式碼,可以看到,我的實體記憶體雖然只有 2GB,但是程式正常分配了 4GB 大小的虛擬記憶體:
我們可以通過下面這條命令檢視程式(test)的虛擬記憶體大小:
# ps aux | grep test
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 7797 0.0 0.0 4198540 352 pts/1 S+ 16:58 0:00 ./test
其中,VSZ 就代表程式使用的虛擬記憶體大小,RSS 代表程式使用的實體記憶體大小。可以看到,VSZ 大小為 4198540,也就是 4GB 的虛擬記憶體。
Swap 機制的作用
前面討論在 32 位/64 位作業系統環境下,申請的虛擬記憶體超過實體記憶體後會怎麼樣?
- 在 32 位作業系統,因為程式最大隻能申請 3 GB 大小的虛擬記憶體,所以直接申請 8G 記憶體,會申請失敗。
- 在 64 位作業系統,因為程式最大隻能申請 128 TB 大小的虛擬記憶體,即使實體記憶體只有 4GB,申請 8G 記憶體也是沒問題,因為申請的記憶體是虛擬記憶體。
程式申請的虛擬記憶體,如果沒有被使用,它是不會佔用物理空間的。當訪問這塊虛擬記憶體後,作業系統才會進行實體記憶體分配。
如果申請實體記憶體大小超過了空閒實體記憶體大小,就要看作業系統有沒有開啟 Swap 機制:
- 如果沒有開啟 Swap 機制,程式就會直接 OOM;
- 如果有開啟 Swap 機制,程式可以正常執行。
什麼是 Swap 機制?
當系統的實體記憶體不夠用的時候,就需要將實體記憶體中的一部分空間釋放出來,以供當前執行的程式使用。那些被釋放的空間可能來自一些很長時間沒有什麼操作的程式,這些被釋放的空間會被臨時儲存到磁碟,等到那些程式要執行時,再從磁碟中恢復儲存的資料到記憶體中。
另外,當記憶體使用存在壓力的時候,會開始觸發記憶體回收行為,會把這些不常訪問的記憶體先寫到磁碟中,然後釋放這些記憶體,給其他更需要的程式使用。再次訪問這些記憶體時,重新從磁碟讀入記憶體就可以了。
這種,將記憶體資料換出磁碟,又從磁碟中恢復資料到記憶體的過程,就是 Swap 機制負責的。
Swap 就是把一塊磁碟空間或者本地檔案,當成記憶體來使用,它包含換出和換入兩個過程:
- 換出(Swap Out) ,是把程式暫時不用的記憶體資料儲存到磁碟中,並釋放這些資料佔用的記憶體;
- 換入(Swap In),是在程式再次訪問這些記憶體的時候,把它們從磁碟讀到記憶體中來;
Swap 換入換出的過程如下圖:
使用 Swap 機制優點是,應用程式實際可以使用的記憶體空間將遠遠超過系統的實體記憶體。由於硬碟空間的價格遠比記憶體要低,因此這種方式無疑是經濟實惠的。當然,頻繁地讀寫硬碟,會顯著降低作業系統的執行速率,這也是 Swap 的弊端。
Linux 中的 Swap 機制會在記憶體不足和記憶體閒置的場景下觸發:
- 記憶體不足:當系統需要的記憶體超過了可用的實體記憶體時,核心會將記憶體中不常使用的記憶體頁交換到磁碟上為當前程式讓出記憶體,保證正在執行的程式的可用性,這個記憶體回收的過程是強制的直接記憶體回收(Direct Page Reclaim)。直接記憶體回收是同步的過程,會阻塞當前申請記憶體的程式。
- 記憶體閒置:應用程式在啟動階段使用的大量記憶體在啟動後往往都不會使用,通過後臺執行的守護程式(kSwapd),我們可以將這部分只使用一次的記憶體交換到磁碟上為其他記憶體的申請預留空間。kSwapd 是 Linux 負責頁面置換(Page replacement)的守護程式,它也是負責交換閒置記憶體的主要程式,它會在空閒記憶體低於一定水位時,回收記憶體頁中的空閒記憶體保證系統中的其他程式可以儘快獲得申請的記憶體。kSwapd 是後臺程式,所以回收記憶體的過程是非同步的,不會阻塞當前申請記憶體的程式。
Linux 提供了兩種不同的方法啟用 Swap,分別是 Swap 分割槽(Swap Partition)和 Swap 檔案(Swapfile),開啟方法可以看這個資料:
- Swap 分割槽是硬碟上的獨立區域,該區域只會用於交換分割槽,其他的檔案不能儲存在該區域上,我們可以使用
Swapon -s
命令檢視當前系統上的交換分割槽; - Swap 檔案是檔案系統中的特殊檔案,它與檔案系統中的其他檔案也沒有太多的區別;
Swap 換入換出的是什麼型別的記憶體?
核心快取的檔案資料,因為都有對應的磁碟檔案,所以在回收檔案資料的時候, 直接寫回到對應的檔案就可以了。
但是像程式的堆、棧資料等,它們是沒有實際載體,這部分記憶體被稱為匿名頁。而且這部分記憶體很可能還要再次被訪問,所以不能直接釋放記憶體,於是就需要有一個能儲存匿名頁的磁碟載體,這個載體就是 Swap 分割槽。
匿名頁回收的方式是通過 Linux 的 Swap 機制,Swap 會把不常訪問的記憶體先寫到磁碟中,然後釋放這些記憶體,給其他更需要的程式使用。再次訪問這些記憶體時,重新從磁碟讀入記憶體就可以了。
接下來,通過兩個實驗,看看申請的實體記憶體超過實體記憶體會怎樣?
- 實驗一:沒有開啟 Swap 機制
- 實驗二:有開啟 Swap 機制
實驗一:沒有開啟 Swap 機制
我的伺服器是 64 位作業系統,但是實體記憶體只有 2 GB,而且沒有 Swap 分割槽:
我們改一下前面的程式碼,使得在申請完 4GB 虛擬記憶體後,通過 memset 函式訪問這個虛擬記憶體,看看在沒有 Swap 分割槽的情況下,會發生什麼?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MEM_SIZE 1024 * 1024 * 1024
int main() {
char* addr[4];
int i = 0;
for(i = 0; i < 4; ++i) {
addr[i] = (char*) malloc(MEM_SIZE);
if(!addr[i]) {
printf("執行 malloc 失敗, 錯誤:%s\n",strerror(errno));
return -1;
}
printf("主執行緒呼叫malloc後,申請1gb大小得記憶體,此記憶體起始地址:0X%x\n", addr[i]);
}
for(i = 0; i < 4; ++i) {
printf("開始訪問第 %d 塊虛擬記憶體(每一塊虛擬記憶體為 1 GB)\n", i + 1);
memset(addr[i], 0, MEM_SIZE);
}
//輸入任意字元後,才結束
getchar();
return 0;
}
執行結果:
可以看到,在訪問第 2 塊虛擬記憶體(每一塊虛擬記憶體是 1 GB)的時候,因為超過了機器的實體記憶體(2GB),程式(test)被作業系統殺掉了。
通過檢視 message 系統日誌,可以發現該程式是被作業系統 OOM killer 機制殺掉了,日誌裡報錯了 Out of memory,也就是發生 OOM(記憶體溢位錯誤)。
什麼是 OOM?
記憶體溢位(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式執行要用到的記憶體大於能提供的最大記憶體。此時程式就執行不了,系統會提示記憶體溢位。
實驗二:有開啟 Swap 機制
我用我的 mac book pro 筆記本做測試,我的筆記本是 64 位作業系統,實體記憶體是 8 GB, 目前 Swap 分割槽大小為 1 GB(注意這個大小不是固定不變的,Swap 分割槽總大小是會動態變化的,當沒有使用 Swap 分割槽時,Swap 分割槽總大小是 0;當使用了 Swap 分割槽,Swap 分割槽總大小會增加至 1 GB;當 Swap 分割槽已使用的大小超過 1 GB 時;Swap 分割槽總大小就會增加到至 2 GB;當 Swap 分割槽已使用的大小超過 2 GB 時;Swap 分割槽總大小就增加至 3GB,如此往復。這個估計是 macos 自己實現的,Linux 的分割槽則是固定大小的,Swap 分割槽不會根據使用情況而自動增長)。
為了方便觀察磁碟 I/O 情況,我們改進一下前面的程式碼,分配完 32 GB虛擬記憶體後(筆記本實體記憶體是 8 GB),通過一個 while 迴圈頻繁訪問虛擬記憶體,程式碼如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MEM_SIZE 32 * 1024 * 1024 * 1024
int main() {
char* addr = (char*) malloc((long)MEM_SIZE);
printf("主執行緒呼叫malloc後,目前共申請了 32gb 的虛擬記憶體\n");
//迴圈頻繁訪問虛擬記憶體
while(1) {
printf("開始訪問 32gb 大小的虛擬記憶體...\n");
memset(addr, 0, (long)MEM_SIZE);
}
return 0;
}
執行結果如下:
可以看到,在有 Swap 分割槽的情況下,即使筆記本實體記憶體是 8 GB,申請並使用 32 GB 記憶體是沒問題,程式正常執行了,並沒有發生 OOM。
從下圖可以看到,程式的記憶體顯示 32 GB(這個不要理解為佔用的實體記憶體,理解為已被訪問的虛擬記憶體大小,也就是在實體記憶體呆過的記憶體大小),系統已使用的 Swap 分割槽達到 2.3 GB。
此時我的膝上型電腦的磁碟開始出現“沙沙”的聲音,通過檢視磁碟的 I/O 情況,可以看到磁碟 I/O 達到了一個峰值,非常高:
有了 Swap 分割槽,是不是意味著程式可以使用的記憶體是無上限的?
當然不是,我把上面的程式碼改成了申請 64GB 記憶體後,當程式申請完 64GB 虛擬記憶體後,使用到 56 GB (這個不要理解為佔用的實體記憶體,理解為已被訪問的虛擬記憶體大小,也就是在實體記憶體呆過的記憶體大小)的時候,程式就被系統 kill 掉了,如下圖:
當系統多次嘗試回收記憶體,還是無法滿足所需使用的記憶體大小,程式就會被系統 kill 掉了,意味著發生了 OOM (PS:我沒有在 macos 系統找到像 linux 系統裡的 /var/log/message 系統日誌檔案,所以無法通過檢視日誌確認是否發生了 OOM)。
總結
至此, 驗證完成了。簡單總結下:
- 在 32 位作業系統,因為程式最大隻能申請 3 GB 大小的虛擬記憶體,所以直接申請 8G 記憶體,會申請失敗。
- 在 64位 位作業系統,因為程式最大隻能申請 128 TB 大小的虛擬記憶體,即使實體記憶體只有 4GB,申請 8G 記憶體也是沒問題,因為申請的記憶體是虛擬記憶體。如果這塊虛擬記憶體被訪問了,要看系統有沒有 Swap 分割槽:
- 如果沒有 Swap 分割槽,因為物理空間不夠,程式會被作業系統殺掉,原因是 OOM(記憶體溢位);
- 如果有 Swap 分割槽,即使實體記憶體只有 4GB,程式也能正常使用 8GB 的記憶體,程式可以正常執行;