閱讀367 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Glibc 的malloc源代碼分析

Glibc malloc 源代碼分析

有人寫了一個測試程序

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>

main()
{
  int alloc_time = 20000;
  char* a[alloc_time];
  char* b[alloc_time];
for(int j=0; j<5; j++)
{
  for(int i=0; i<alloc_time; i++)
  {
    a[i] = (char*)malloc(52722);
    memset(a[i], 1, 52722);
    b[i] = (char*)malloc(1);
    memset(b[i], 1, 1);
  }
  for(int i=0; i<alloc_time; i++)
  {
    free(a[i]);
    free(b[i]);
  }
}
  while(1)
  {
    sleep(10);
  }
}

發現 該程序在測試機上運行會占用 1G 內存,不釋放,為了解決這個問題,特別去研究了一下glibcmalloc 的源代碼。

 

 

 

 

一.對於小於 128k 的塊在 heap 中分配。

1. 堆是通過 brk 的方式來增長或壓縮的,如果在現有的堆中不能找到合適的 chunk ,會通過增長堆的方式來滿足分配,如果堆頂的空閑塊超過一定的閥值會收縮堆,所以隻要堆頂的空間沒釋放,堆是一直不會收縮的。

2. 堆中的分配信息是通過兩個方式來記錄。

第一.是通過 chunk 的頭, chunk 中的頭一個字是記錄前一個 chunk 的大小,第二個字是記錄當前 chunk 的大小和一些標誌位,從第三個字開始是要使用的內存。所以通過內存地址可以找到 chunk ,通過 chunk 也可以找到內存地址。還可以找到相鄰的下一個 chunk ,和相鄰的前一個 chunk 一個堆完全是由 n chunk 組成

第二.是 3 種隊列記錄 ,隻用空閑 chunk 才會出現在隊列中,使用的 chunk 不會出現在隊列中。如果內存塊是空閑的它會掛到其中一個隊列中,它是通過內存複用的方式,使用空閑 chunk 的第 3 個字和第 4 個字當作它的前鏈和後鏈(變長塊是第 5 個字和第 6 個字),省的再分配空間給它。

第一種隊列是 bins bins 128 個隊列,前 64 個隊列是定長的,每隔 8 個字節大小的塊分配在一個隊列,後麵的 64 個隊列是不定長的,就是在一個範圍長度的都分配在一個隊列中。所有長度小於 512 字節(大約)的都分配在定長的隊列中 。後麵的 64 個隊列是變長的隊列,每個隊列中的 chunk 都是從小到大排列的。

第二種隊列是 unsort 隊列(隻有一個隊列),(是一個緩衝)所有 free 下來的如果要進入 bins 隊列中都要經過 unsort 隊列。

第三種隊列是 fastbins ,大約有 10 個定長隊列,(是一個高速緩衝)所有 free 下來的並且長度是小於 80 chunk 就會進入這種隊列中。進入此隊列的 chunk free 的時候並不修改使用位,目的是為了避免被相鄰的塊合並掉。

3.malloc 的步驟

l         先在 fastbins 中找,如果能找到,從隊列中取下後(不需要再置使用位為 1 了)立刻返回。

l         判斷需求的塊是否在小箱子 bins 的前 64 bin )範圍,如果在小箱子的範圍,並且剛好有需求的塊,則直接返回內存地址;如果範圍在大箱子( bins 的後 64 bin )裏,則觸發 consolidate 。(因為在大箱子找一般都要切割,所以要優先合並,避免過多碎片)

l         然後在 unsort 中取出一個 chunk ,如果能找到剛好和想要的 chunk 相同大小的 chunk ,立刻返回,如果不是想要 chunk 大小的 chunk ,就把他插入到 bins 對應的隊列中去。轉 3 ,直到清空,或者一次循環了 10000 次。

l         然後才在 bins 中找,找到一個最小的能符合需求的 chunk 從隊列中取下,如果剩下的大小還能建一個 chunk ,就把 chunk 分成兩個部分,把剩下的 chunk 插入到 unsort 隊列中去,把 chunk 的內存地址返回。

l         topchunk (是堆頂的一個 chunk ,不會放到任何一個隊列裏的)找,如果能切出符合要求的,把剩下的一部分當作 topchunk ,然後返回內存地址。

l         如果 fastbins 不為空,觸發 consolidate 即把所有的 fanbins 清空 (是把 fanbins 的使用位置 0 ,把相鄰的塊合並起來,然後掛到 unsort 隊列中去),然後繼續第 3 步。

l         還找不到話就調用 sysalloc ,其實就是增長堆了。然後返回內存地址。

4.free 的步驟

l         如果和 topchunk 相鄰,直接和 topchunk 合並,不會放到其他的空閑隊列中去。

l         如果釋放的大小小於 80 字節,就把它掛到 fastbins 中去,使用位仍然為 1 ,當然更不會去合並相鄰塊。

l         如果釋放塊大小介於 80-128k ,把 chunk 的使用位置成 0 ,然後試圖合並相鄰塊,掛到 unsort 隊列中去,如果合並後的大小大於 64k ,也會觸發 consolidate ,(可能是周圍比較多小塊了吧),然後才試圖去收縮堆。(收縮堆的條件是當前 free 的塊大小加上前後能合並 chunk 的大小大於 64k ,並且要堆頂的大小要達到閥值,才有可能收縮堆)

l         對於大於 128k 的塊,直接 munmap

 

 

 

二.大於 128k 的塊通過 mmap 的方式來分配。

 

 

 

 

根據以上的分析,我們可以得出為什麼會還占用 1G 的內存。

測試程序有兩重循環,內層循環先分配 1G ,然後全部釋放,外層執行這個步驟 5 次,但為什麼最後一次釋放會不成功呢。

主要是因為按照這個程序分配後,內存變成由小塊和大塊交替出現,釋放小塊的時候,直接把小塊放到 fastbins 中去,而且他的使用位還是 1 ,釋放大塊的時候,它試圖合並相鄰的塊,但是和它相鄰的塊的使用位還是 1 ,所以它不能把相鄰的塊合並去來,而且釋放的塊的大小小於 64k 也不會觸發 consolidate ,即不會把 fastbins 清空 ,所以當所有的塊都被釋放完後,所有的小塊都在 fastbins 裏麵,使用位都為 1 ,大塊都掛在 unsort 隊列裏麵。全部都無法合並。所以使用的堆更加無法壓縮。

 

如果在外循環後麵再分配 2000 字節然後釋放的話,所有內存將全部清空。這是因為在申請 2000 字節的時候,由 malloc 的第二步,程序會先調用 consolidate ,即把所有的 fastbins 清空,同時把相鄰的塊合並起來,等到所有的 fastbins 清空的時候,所有的塊也被合並去來了,然後調用 free2000 字節的時候,這塊被將被合並起來 ,成為 topchunk ,並且大小遠大於 64k ,所有堆將會收縮,內存歸還給係統。

 

轉帖自:https://blog.chinaunix.net/u3/94916/showart_1908306.html

最後更新:2017-04-02 03:42:38

  上一篇:go Dtree樹型Js控件測試例子
  下一篇:go 一步一步SEO 之奇淫異術