天天看點

malloc 是如何配置設定記憶體的?

作者:小林coding

圖解計算機基礎網站:​​https://xiaolincoding.com/​​

大家好,我是小林。

很早之前寫了一篇圖解虛拟記憶體的文章:​​真棒!20 張圖揭開記憶體管理的迷霧,瞬間豁然開朗​​

最近想多寫一些記憶體管理的文章,這次我們就以 malloc 動态記憶體配置設定為切入點,我在文中也做了小實驗:

  • malloc 是如何配置設定記憶體的?
  • malloc 配置設定的是實體記憶體嗎?
  • malloc(1) 會配置設定多大的記憶體?
  • free 釋放記憶體,會歸還給作業系統嗎?
  • free() 函數隻傳入一個記憶體位址,為什麼能知道要釋放多大的記憶體?

發車!

Linux 程序的記憶體分布長什麼樣?

在 Linux 作業系統中,虛拟位址空間的内部又被分為核心空間和使用者空間兩部分,不同位數的系統,位址空間的範圍也不同。比如最常見的 32 位和 64 位系統,如下所示:

malloc 是如何配置設定記憶體的?

通過這裡可以看出:

  • ​32​

    ​​ 位系統的核心空間占用​

    ​1G​

    ​​,位于最高處,剩下的​

    ​3G​

    ​ 是使用者空間;
  • ​64​

    ​​ 位系統的核心空間和使用者空間都是​

    ​128T​

    ​,分别占據整個記憶體空間的最高和最低處,剩下的中間部分是未定義的。

再來說說,核心空間與使用者空間的差別:

  • 程序在使用者态時,隻能通路使用者空間記憶體;
  • 隻有進入核心态後,才可以通路核心空間的記憶體;

雖然每個程序都各自有獨立的虛拟記憶體,但是每個虛拟記憶體中的核心位址,其實關聯的都是相同的實體記憶體。這樣,程序切換到核心态後,就可以很友善地通路核心空間記憶體。

malloc 是如何配置設定記憶體的?

接下來,進一步了解虛拟空間的劃分情況,使用者空間和核心空間劃分的方式是不同的,核心空間的分布情況就不多說了。

我們看看使用者空間分布的情況,以 32 位系統為例,我畫了一張圖來表示它們的關系:

通過這張圖你可以看到,使用者空間記憶體從低到高分别是 6 種不同的記憶體段:

malloc 是如何配置設定記憶體的?
  • 程式檔案段,包括二進制可執行代碼;
  • 已初始化資料段,包括靜态常量;
  • 未初始化資料段,包括未初始化的靜态變量;
  • 堆段,包括動态配置設定的記憶體,從低位址開始向上增長;
  • 檔案映射段,包括動态庫、共享記憶體等,從低位址開始向上增長(跟硬體和核心版本有關 );
  • 棧段,包括局部變量和函數調用的上下文等。棧的大小是固定的,一般是​

    ​8 MB​

    ​。當然系統也提供了參數,以便我們自定義大小;

在這 6 個記憶體段中,堆和檔案映射段的記憶體是動态配置設定的。比如說,使用 C 标準庫的 ​

​malloc()​

​​ 或者 ​

​mmap()​

​ ,就可以分别在堆和檔案映射段動态配置設定記憶體。

malloc 是如何配置設定記憶體的?

實際上,malloc() 并不是系統調用,而是 C 庫裡的函數,用于動态配置設定記憶體。

malloc 申請記憶體的時候,會有兩種方式向作業系統申請堆記憶體。

  • 方式一:通過 brk() 系統調用從堆配置設定記憶體
  • 方式二:通過 mmap() 系統調用在檔案映射區域配置設定記憶體;

方式一實作的方式很簡單,就是通過 brk() 函數将「堆頂」指針向高位址移動,獲得新的記憶體空間。如下圖:

malloc 是如何配置設定記憶體的?

方式二通過 mmap() 系統調用中「私有匿名映射」的方式,在檔案映射區配置設定一塊記憶體,也就是從檔案映射區“偷”了一塊記憶體。如下圖:

malloc 是如何配置設定記憶體的?
什麼場景下 malloc() 會通過 brk() 配置設定記憶體?又是什麼場景下通過 mmap() 配置設定記憶體?

malloc() 源碼裡預設定義了一個門檻值:

  • 如果使用者配置設定的記憶體小于 128 KB,則通過 brk() 申請記憶體;
  • 如果使用者配置設定的記憶體大于 128 KB,則通過 mmap() 申請記憶體;

malloc() 配置設定的是實體記憶體嗎?

不是的,malloc() 配置設定的是虛拟記憶體。

如果配置設定後的虛拟記憶體沒有被通路的話,是不會将虛拟記憶體不會映射到實體記憶體,這樣就不會占用實體記憶體了。

隻有在通路已配置設定的虛拟位址空間的時候,作業系統通過查找頁表,發現虛拟記憶體對應的頁沒有在實體記憶體中,就會觸發缺頁中斷,然後作業系統會建立虛拟記憶體和實體記憶體之間的映射關系。

malloc(1) 會配置設定多大的虛拟記憶體?

malloc() 在配置設定記憶體的時候,并不是老老實實按使用者預期申請的位元組數來配置設定記憶體空間大小,而是會預配置設定更大的空間作為記憶體池。

具體會預配置設定多大的空間,跟 malloc 使用的記憶體管理器有關系,我們就以 malloc 預設的記憶體管理器(Ptmalloc2)來分析。

接下裡,我們做個實驗,用下面這個代碼,通過 malloc 申請 1位元組的記憶體時,看看作業系統實際配置設定了多大的記憶體空間。

#include <stdio.h>
#include <malloc.h>

int main() {
  printf("使用cat /proc/%d/maps檢視記憶體配置設定\n",getpid());
  
  //申請1位元組的記憶體
  void *addr = malloc(1);
  printf("此1位元組的記憶體起始位址:%x\n", addr);
  printf("使用cat /proc/%d/maps檢視記憶體配置設定\n",getpid());
 
  //将程式阻塞,當輸入任意字元時才往下執行
  getchar();

  //釋放記憶體
  free(addr);
  printf("釋放了1位元組的記憶體,但heap堆并不會釋放\n");
  
  getchar();
  return 0;
}      

執行代碼:

malloc 是如何配置設定記憶體的?

我們可以通過 /proc//maps 檔案檢視程序的記憶體分布情況。我在 maps 檔案通過此 1 位元組的記憶體起始位址過濾出了記憶體位址的範圍。

[root@xiaolin ~]# cat /proc/3191/maps | grep d730
00d73000-00d94000 rw-p 00000000 00:00 0                                  [heap]      

這個例子配置設定的記憶體小于 128 KB,是以是通過 brk() 系統調用向堆空間申請的記憶體,是以可以看到最右邊有 [heap] 的辨別。

可以看到,堆空間的記憶體位址範圍是 00d73000-00d94000,這個範圍大小是 132KB,也就說明了 malloc(1) 實際上預配置設定 132K 位元組的記憶體。

可能有的同學注意到了,程式裡列印的記憶體起始位址是 ​

​d73010​

​​,而 maps 檔案顯示堆記憶體空間的起始位址是 ​

​d73000​

​​,為什麼會多出來 ​

​0x10​

​ (16位元組)呢?這個問題,我們先放着,後面會說。

free 釋放記憶體,會歸還給作業系統嗎?

我們在上面的程序往下執行,看看通過 free() 函數釋放記憶體後,堆記憶體還在嗎?

malloc 是如何配置設定記憶體的?

從下圖可以看到,通過 free 釋放記憶體後,堆記憶體還是存在的,并沒有歸還給作業系統。

malloc 是如何配置設定記憶體的?

這是因為與其把這 1 位元組釋放給作業系統,不如先緩存着放進 malloc 的記憶體池裡,當程序再次申請 1 位元組的記憶體時就可以直接複用,這樣速度快了很多。

當然,當程序退出後,作業系統就會回收程序的所有資源。

上面說的 free 記憶體後堆記憶體還存在,是針對 malloc 通過 brk() 方式申請的記憶體的情況。

如果 malloc 通過 mmap 方式申請的記憶體,free 釋放記憶體後就會歸歸還給作業系統。

我們做個實驗驗證下, 通過 malloc 申請 128 KB 位元組的記憶體,來使得 malloc 通過 mmap 方式來配置設定記憶體。

#include <stdio.h>
#include <malloc.h>

int main() {
  //申請1位元組的記憶體
  void *addr = malloc(128*1024);
  printf("此128KB位元組的記憶體起始位址:%x\n", addr);
  printf("使用cat /proc/%d/maps檢視記憶體配置設定\n",getpid());

  //将程式阻塞,當輸入任意字元時才往下執行
  getchar();

  //釋放記憶體
  free(addr);
  printf("釋放了128KB位元組的記憶體,記憶體也歸還給了作業系統\n");

  getchar();
  return 0;
}      

執行代碼:

malloc 是如何配置設定記憶體的?

檢視程序的記憶體的分布情況,可以發現最右邊沒有 [head] 标志,說明是通過 mmap 以匿名映射的方式從檔案映射區配置設定的匿名記憶體。

malloc 是如何配置設定記憶體的?

然後我們釋放掉這個記憶體看看:

malloc 是如何配置設定記憶體的?

再次檢視該 128 KB 記憶體的起始位址,可以發現已經不存在了,說明歸還給了作業系統。

malloc 是如何配置設定記憶體的?

對于 「malloc 申請的記憶體,free 釋放記憶體會歸還給作業系統嗎?」這個問題,我們可以做個總結了:

  • malloc 通過brk()方式申請的記憶體,free 釋放記憶體的時候,并不會把記憶體歸還給作業系統,而是緩存在 malloc 的記憶體池中,待下次使用;
  • malloc 通過mmap()方式申請的記憶體,free 釋放記憶體的時候,會把記憶體歸還給作業系統,記憶體得到真正的釋放。

為什麼不全部使用 mmap 來配置設定記憶體?

因為向作業系統申請記憶體,是要通過系統調用的,執行系統調用是要進入核心态的,然後在回到使用者态,運作态的切換會耗費不少時間。

是以,申請記憶體的操作應該避免頻繁的系統調用,如果都用 mmap 來配置設定記憶體,等于每次都要執行系統調用。

另外,因為 mmap 配置設定的記憶體每次釋放的時候,都會歸還給作業系統,于是每次 mmap 配置設定的虛拟位址都是缺頁狀态的,然後在第一次通路該虛拟位址的時候,就會觸發缺頁中斷。

也就是說,頻繁通過 mmap 配置設定的記憶體話,不僅每次都會發生運作态的切換,還會發生缺頁中斷(在第一次通路虛拟位址後),這樣會導緻 CPU 消耗較大。

為了改進這兩個問題,malloc 通過 brk() 系統調用在堆空間申請記憶體的時候,由于堆空間是連續的,是以直接預配置設定更大的記憶體來作為記憶體池,當記憶體釋放的時候,就緩存在記憶體池中。

等下次在申請記憶體的時候,就直接從記憶體池取出對應的記憶體塊就行了,而且可能這個記憶體塊的虛拟位址與實體位址的映射關系還存在,這樣不僅減少了系統調用的次數,也減少了缺頁中斷的次數,這将大大降低 CPU 的消耗。

既然 brk 那麼牛逼,為什麼不全部使用 brk 來配置設定?

前面我們提到通過 brk 從堆空間配置設定的記憶體,并不會歸還給作業系統,那麼我們那考慮這樣一個場景。

如果我們連續申請了 10k,20k,30k 這三片記憶體,如果 10k 和 20k 這兩片釋放了,變為了空閑記憶體空間,如果下次申請的記憶體小于 30k,那麼就可以重用這個空閑記憶體空間。

malloc 是如何配置設定記憶體的?

但是如果下次申請的記憶體大于 30k,沒有可用的空閑記憶體空間,必須向 OS 申請,實際使用記憶體繼續增大。

是以,随着系統頻繁地 malloc 和 free ,尤其對于小塊記憶體,堆内将産生越來越多不可用的碎片,導緻“記憶體洩露”。而這種“洩露”現象使用 valgrind 是無法檢測出來的。

是以,malloc 實作中,充分考慮了 sbrk 和 mmap 行為上的差異及優缺點,預設配置設定大塊記憶體 (128KB) 才使用 mmap 配置設定記憶體空間。

free() 函數隻傳入一個記憶體位址,為什麼能知道要釋放多大的記憶體?

還記得,我前面提到, malloc 傳回給使用者态的記憶體起始位址比程序的堆空間起始位址多了 16 位元組嗎?

這個多出來的 16 位元組就是儲存了該記憶體塊的描述資訊,比如有該記憶體塊的大小。

malloc 是如何配置設定記憶體的?

繼續閱讀