撥開由問題《Linux下malloc最大可申請的記憶體》帶來的重重疑雲

我叫平沢唯發表於2021-11-24

今天閱讀相關書籍的時候看到 "程式中堆的最大申請數量" 這一問題,我們知道使用malloc分配記憶體是在堆Heap裡面分配的,如果一臺機器一共有8GB實體記憶體,空閒5GB,那麼我們使用malloc( )就一定能夠申請到這5GB記憶體嗎?理論上來說確實如此,因為這些記憶體未被其它程式使用。但實際測試出來結果卻可能令人疑惑。

本文測試環境如下:

1 qi@qi:~$ uname -a
2 Linux qi 5.4.0-89-generic #100~18.04.1-Ubuntu SMP Wed Sep 29 10:59:42 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux 

 


 一、首先需要考慮的幾個問題

  1. 我們使用malloc( )申請到的是實體記憶體嗎?
  2. 使用malloc( )能申請到的只有8GB的實體記憶體嗎?
  3. malloc( )申請到的記憶體大小全都可以被用來memset( )嗎?

以上三個問題,正是本次所要討論的內容。現在假定認為以上三個陳述均正確,那麼我們可以用以下程式測試malloc( )可以申請的記憶體大小:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 
 5 unsigned long long int maximum = 0;
 6 
 7 int main (int argc, char* argv[])
 8 {
 9     unsigned int block_size[] = {1024*1024, 1024, 1};
10     int i, count;
11 
12     for( int i=0; i<3; i++) {
13         for(count=1;; count++) {
14             void *block = malloc(maximum + block_size[i]*count);
15             if( block ) {
16                 //memset(block, 0, maximum + block_size[i]*count);
17                 free(block);
18                 maximum = maximum + block_size[i]*count;
19             } else {
20                 break;
21             }
22         }
23     }
24 
25     printf("maximum malloc size is %llu bytes \n", maximum);
26 
27     return 0;
28 }

 執行以上程式,得到輸出為:

1 root@qi:/home/qi/test_park/elf_load# ./main 
2 maximum malloc size is 24587279333 bytes

 可以看到,以上測試程式最大申請到了 22.9GB 的記憶體,但是我的機器上實際記憶體有多少呢?如下:

1 qi@qi:~/test_park/elf_load$ free
2               total        used        free      shared  buff/cache   available
3 Mem:        8011016     2373760     3517884      719508     2119372     4654640
4 Swap:      15999996           0    15999996

很明顯,機器上最大的實體記憶體也沒到8GB,如果你瞭解swap 交換空間,可能會說Mem項和Swap項的total加起來似乎正好是22.9GB,但是另外一個問題有來了,那就是這些記憶體或者交換空間並不是全部空閒,包括系統核心和系統介面等等也要佔用一部分實體記憶體,所以我們看到Mem項的 "available"的可用記憶體只有大約4.5GB,所以結果就是,malloc( )申請到的記憶體數量是遠遠大於我們實際的實體記憶體的。既然malloc( )函式的實際輸出和我們的預期不相符,那是不是我們哪裡用錯了呢?不妨使用"man malloc"檢視對其的官方解釋:

1 NOTES
2        By default, Linux follows an optimistic memory allocation strategy.  This means that when malloc() returns non-NULL there is no guarantee that the mem‐
3        ory really is available.  In case it turns out that the system is out of memory, one or more processes will be killed by  the  OOM  killer.   For  more
4        information,  see  the  description  of /proc/sys/vm/overcommit_memory and /proc/sys/vm/oom_adj in proc(5), and the Linux kernel source file Documenta‐
5        tion/vm/overcommit-accounting.

 果不其然,Note中說明了就算malloc( )返回非NULL指標也不能保證該指標指向的記憶體區域全都可以被該程式使用。那麼為什麼會這樣呢?後面有提示,首先涉及到的最重要的一個設定就是 "/proc/sys/vm/overcommit_memory" 這一個檔案,使用 "man proc" 找到有關其的說明:

 1        /proc/sys/vm/overcommit_memory
 2               This file contains the kernel virtual memory accounting mode.  Values are:
 3 
 4                      0: heuristic overcommit (this is the default)
 5                      1: always overcommit, never check
 6                      2: always check, never overcommit
 7 
 8               In mode 0, calls of mmap(2) with MAP_NORESERVE are not  checked,  and  the  default
 9               check is very weak, leading to the risk of getting a process "OOM-killed".
10 
11               In mode 1, the kernel pretends there is always enough memory, until memory actually
12               runs out.  One use case for this mode is  scientific  computing  applications  that
13               employ  large  sparse  arrays.   In Linux kernel versions before 2.6.0, any nonzero
14               value implies mode 1.
15 
16               In mode 2 (available since Linux 2.6), the total virtual address space that can  be
17               allocated (CommitLimit in /proc/meminfo) is calculated as
18 
19                   CommitLimit = (total_RAM - total_huge_TLB) *
20                                 overcommit_ratio / 100 + total_swap

可以看到,如果該檔案內容為0,mmap(malloc的內部呼叫)將不檢查,有導致使用不存在記憶體的風險,如果檔案內容為1,則malloc( )可以申請的記憶體可以非常大,我的機器上經過測試可以達到90T,如果該檔案內容為2,那麼所有可以申請的記憶體為 "CommitLimit",具體可以通過公式或者 "cat /proc/meminfo | grep Limit"檢視大小。那麼這就能說通為什麼上面的程式可以malloc( )出22GB多的記憶體了,檢視 "/proc/sys/vm/overcommit_memory" 果不其然,內容為0:

1 root@qi:/home/qi/test_park/elf_load# cat /proc/sys/vm/overcommit_memory 
2 0

以上回答了第2個問題中的一部分,那就是某些設定下,malloc( )可以申請到超出機器實體記憶體的大小,為什麼說是一部分呢,因為可申請的記憶體不僅和上述設定相關,還和機器的swap space相關,如果你不瞭解或者沒聽過( 事實上在你給你機器裝Linux系統的時候應該碰到過,那就是磁碟分割槽的時候會有一個swap設定)swap空間,只需要知道它是一種掛載在物理硬碟上,用來存放一些不太頻繁使用的記憶體,是一種低速的實體記憶體的擴充套件,當實體記憶體不夠用時,原先一些實體記憶體中不常訪問的內容會被轉移到這裡以讓出空間給其它程式。所以swap空間也可以被malloc( )申請到。

由此,第2個問題得到了全部的解答。這個時候你可能會說,第1個問題應該也有答案了,因為malloc( )不僅申請了8GB的實體記憶體,還申請了15GB的swap硬碟空間作為擴充套件記憶體,甚至還可以申請大約90TB的不存在的記憶體,所以第一個問題就解決了嗎?

其實對,但也不全對,因為malloc( )這個時候申請了記憶體,但沒有完全申請,這就涉及到一個叫做 "Lazy Allocation" 的東東,類似於fork的寫時複製機制,當你使用malloc( )時,系統並沒有真正從實體記憶體中分配,而是等到程式要操作時才提供allocation,這也就解釋了我們剛開頭申請了22.9GB的記憶體都還沒有報段錯誤的原因。只有當你access這個記憶體區域的時候才會真正分配,所以我們可以大膽的在程式裡面加上memset,把上面貼出的程式碼的memset那一行取消掉註釋,然後再執行。如果你不想等太久,可以像我這樣:

1 root@qi:/home/qi/test_park/elf_load# echo 0 >  /proc/sys/vm/overcommit_memory 
2 root@qi:/home/qi/test_park/elf_load# swapoff -a
3 root@qi:/home/qi/test_park/elf_load# echo 2 >  /proc/sys/vm/overcommit_memory

以上命令是把交換空間禁用,這樣就可以減少可使用的記憶體了,關閉交換空間後,如果/proc/sys/vm/overcommit_memory內容為0,那麼你可以malloc( )的記憶體大小應該為8GB左右,但是不是每一個位元組都可以memset,大可以測試一下,會發現memset了6~7GB的記憶體空間後程式報錯異常退出,這是因為這個時候可使用的記憶體也就這麼大,這種情況下隨意使用malloc( )申請到的記憶體是不安全的。如果/proc/sys/vm/overcommit_memory內容為2,那麼這個時候可申請的記憶體就得看 "CommitLimit" 了,在我的機器上測試是隻能申請1.5GB左右,這種情況下無論如何也不會訪問非法記憶體區域了,但是一個缺點是不能使用全部的空閒記憶體,只能修改相應的設定。

那麼該如何知道實際可用的記憶體大小呢?一種解決方案是檢視 "/proc/meminfo" 中的available memory,乘個安全係數再來申請。

以上,三個問題全都被解決,離專業的linuxer又近了一步~

 

相關文章