一篇文章徹底講懂malloc的實現(ptmalloc)

一、前言

C語言提供了動態內存管理功能, 在C語言中, 程序員可以使用 malloc() 和 free() 函數顯式的分配和釋放內存. 關於 malloc() 和free() 函數, C語言標準只是規定了它們需要實現的功能, 而沒有對實現方式有什麼限制, 這多少讓那些追根究底的人感到有些許迷茫, 比如對於 free() 函數, 它規定一旦一個內存區域被釋放掉, 那麼就不應該再對其進行任何引用, 任何對釋放區域的引用都會導致不可預知的後果 (unperdictable effects). 那麼, 到底是什麼樣的不可預知後果呢? 這完全取決於內存分配器(memory allocator)使用的算法. 這篇文章試圖對 Linux glibc 提供的 allocator 的工作方式進行一些描述, 並希望可以解答上述類似的問題. 雖然這裏的描述侷限於特定的平臺, 但一般的事實是, 相同功能的軟件基本上都會採用相似的技術. 這裏所描述的原理也許在別的環境下會仍然有效. 另外還要強調的一點是, 本文只是側重於一般原理的描述, 而不會過分糾纏於細節, 如果需要特定的細節知識, 請參考特定 allocator 的源代碼. 最後, 本文描述的硬件平臺是 Intel 80x86, 其中涉及的有些原理和數據可能是平臺相關的.

二、內存佈局

介紹ptmalloc之前,我們先了解一下內存佈局,以x86的32位系統爲例:

從上圖可以看到,棧至頂向下擴展,堆至底向上擴展, mmap 映射區域至頂向下擴展。 mmap 映射區域和堆相對擴展,直至耗盡虛擬地址空間中的剩餘區域,這種結構便於 C 運行時庫使用 mmap 映射區域和堆進行內存分配。

三、brk(sbrk)和mmap函數

首先,linux系統向用戶提供申請的內存有brk(sbrk)和mmap函數。下面我們先來了解一下這幾個函數。

1、brk() 和 sbrk()

#include <unistd.h>
int brk( const void *addr )
void* sbrk ( intptr_t incr );

兩者的作用是擴展heap的上界brk
Brk()的參數設置爲新的brk上界地址,成功返回1,失敗返回0;
Sbrk()的參數爲申請內存的大小,返回heap新的上界brk的地址

2、mmap()

#include <sys/mman.h>
void *mmap(void *addr, size\_t length, int prot, int flags, int fd, off\_t offset);
int munmap(void *addr, size_t length);

Mmap的第一種用法是映射此盤文件到內存中;第二種用法是匿名映射,不映射磁盤文件,而向映射區申請一塊內存。
Malloc使用的是mmap的第二種用法(匿名映射)。
Munmap函數用於釋放內存。

四、Allocator

GNU Libc 的內存分配器( allocator ) — ptmalloc 起源於 Doug Lea 的 malloc (請參看[1]). ptmalloc 實現了 malloc() , free() 以及一組其它的函數. 以提供動態內存管理的支持. allocator 處在用戶程序和內核之間, 它響應用戶的分配請求, 向操作系統申請內存, 然後將其返回給用戶程序, 爲了保持高效的分配, allocator 一般都會預先分配一塊大於用戶請求的內存, 並通過某種算法管理這塊內存. 來滿足用戶的內存分配要求, 用戶 free 掉的內存也並不是立即就返回給操作系統, 相反, allocator 會管理這些被 free 掉的空閒空間, 以應對用戶以後的內存分配要求. 也就是說, allocator 不但要管理已分配的內存塊, 還需要管理空閒的內存塊, 當響應用戶分配要求時, allocator 會首先在空閒空間中尋找一塊合適的內存給用戶, 在空閒空間中找不到的情況下才分配一塊新的內存. 爲實現一個高效的 allocator, 需要考慮很多的因素. 比如, allocator 本身管理內存塊所佔用的內存空間必須很小, 分配算法必須要足夠的快. Jonathan Bartlett 給出了一個簡單的 allocator 實現[2], 事先看看或許會對理解本文有所幫助. 另外插一句, Jonathan Bartlett 的書 “Programming from Ground Up” 對想要了解 linux 彙編和工作方式的入門者是個不錯的選擇.

五、Malloc實現原理

因爲brk、sbrk、mmap都屬於系統調用,若每次申請內存,都調用這三個,那麼每次都會產生系統調用,影響性能;其次,這樣申請的內存容易產生碎片,因爲堆是從低地址到高地址,如果高地址的內存沒有被釋放,低地址的內存就不能被回收。
  
所以malloc採用的是內存池的管理方式(ptmalloc),Ptmalloc 採用邊界標記法將內存劃分成很多塊,從而對內存的分配與回收進行管理。爲了內存分配函數malloc的高效性,ptmalloc會預先向操作系統申請一塊內存供用戶使用,當我們申請和釋放內存的時候,ptmalloc會將這些內存管理起來,並通過一些策略來判斷是否將其回收給操作系統。這樣做的最大好處就是,使用戶申請和釋放內存的時候更加高效,避免產生過多的內存碎片。

1、chunk 內存塊的基本組織單元

在 ptmalloc 的實現源碼中定義結構體 malloc_chunk 來描述這些塊。malloc_chunk 定義如下:

struct malloc_chunk {  
  INTERNAL_SIZE_T      prev_size;    /* Size of previous chunk (if free).  */  
  INTERNAL_SIZE_T      size;         /* Size in bytes, including overhead. */  
  
  struct malloc_chunk* fd;           /* double links -- used only if free. */  
  struct malloc_chunk* bk;  
  
  /* Only used for large blocks: pointer to next larger size.  */  
  struct malloc_chunk* fd_nextsize;      /* double links -- used only if free. */  
  struct malloc_chunk* bk_nextsize; 
};  

chunk 的定義相當簡單明瞭,對各個域做一下簡單介紹 :
  prev_size: 如果前一個 chunk 是空閒的,該域表示前一個 chunk 的大小,如果前一個 chunk 不空閒,該域無意義。 (這裏的描述有點模糊,一段連續的內存被分成多個chunk,prev_size記錄的就是相鄰的前一個chunk的size,知道當前chunk的地址,減去prev_size便是前一個chunk的地址。prev_size主要用於相鄰空閒chunk的合併)
  size :當前 chunk 的大小,並且記錄了當前 chunk 和前一個 chunk 的一些屬性,包括前一個 chunk 是否在使用中,當前 chunk 是否是通過 mmap 獲得的內存,當前 chunk 是否屬於非主分配區。
   fd 和 bk : 指針 fd 和 bk 只有當該 chunk 塊空閒時才存在,其作用是用於將對應的空閒 chunk 塊加入到空閒chunk 塊鏈表中統一管理,如果該 chunk 塊被分配給應用程序使用,那麼這兩個指針也就沒有用(該 chunk 塊已經從空閒鏈中拆出)了,所以也當作應用程序的使用空間,而不至於浪費。
  fd_nextsize 和 bk_nextsize: 當前的 chunk 存在於 large bins 中時, large bins 中的空閒 chunk 是按照大小排序的,但同一個大小的 chunk 可能有多個,增加了這兩個字段可以加快遍歷空閒 chunk ,並查找滿足需要的空閒 chunk , fd_nextsize 指向下一個比當前 chunk 大小大的第一個空閒 chunk , bk_nextszie 指向前一個比當前 chunk 大小小的第一個空閒 chunk 。(同一大小的chunk可能有多塊,在總體大小有序的情況下,要想找到下一個比自己大或小的chunk,需要遍歷所有相同的chunk,所以纔有fd_nextsize和bk_nextsize這種設計) 如果該 chunk 塊被分配給應用程序使用,那麼這兩個指針也就沒有用(該chunk 塊已經從 size 鏈中拆出)了,所以也當作應用程序的使用空間,而不至於浪費。

(下面馬上可以看到,當chunk爲空時纔有fd、bk、fd_nextsize、bd_nextsize四個指針,當chunk不爲空,這四個指針的空間是直接交給用戶使用的)

2、chunk的結構

chunk的結構可以分爲使用中的chunk和空閒的chunk。使用中的chunk和空閒的chunk數據結構基本相同,但是會有一些設計上的小技巧,巧妙的節省了內存。

a、使用中的chunk

說明:
  1、 chunk指針指向chunk開始的地址;mem指針指向用戶內存塊開始的地址。
  2、 p=0時,表示前一個chunk爲空閒,prev_size纔有效
  3、p=1時,表示前一個chunk正在使用,prev_size無效 p主要用於內存塊的合併操作;ptmalloc 分配的第一個塊總是將p設爲1, 以防止程序引用到不存在的區域
  4、M=1 爲mmap映射區域分配;M=0爲heap區域分配
  5、 A=0 爲主分配區分配;A=1 爲非主分配區分配。

b、空閒的chunk

說明:
  1、當chunk空閒時,其M狀態是不存在的,只有AP狀態(因爲M表示是由brk還是mmap分配的內存,而mmap分配的內存free時直接ummap,不會放到空閒鏈表中。換言之空閒鏈表中的都死brk分配的,所以不用額外記錄)
  2、原本是用戶數據區的地方存儲了四個指針,
   指針fd指向後一個空閒的chunk,而bk指向前一個空閒的chunk,malloc通過這兩個指針將大小相近的chunk連成一個雙向鏈表。
   在large bin中的空閒chunk,還有兩個指針,fd_nextsize和bk_nextsize,用於加快在large bin中查找最近匹配的空閒chunk。不同的chunk鏈表又是通過bins或者fastbins來組織的。

c、chunk中的空間複用

爲了使得 chunk 所佔用的空間最小, ptmalloc 使用了空間複用, 一個 chunk 或者正在被使用, 或者已經被 free 掉, 所以 chunk 的中的一些域可以在使用狀態和空閒狀態表示不同的意義, 來達到空間複用的效果. 空閒時, 一個 chunk 中至少要4個 size_t 大小的空間, 用來存儲 prev_size, size , fd 和 bk , 也就是16 bytes(??爲什麼不是6個size_t呢?不是還有fd_nextsize和bk_nextsize嗎?——並不是所有bin中都需要這兩個指針,比如在fast_bin中,每隔8個Byte就有一個鏈表,每個鏈表中的所有chunk的size都是一樣的,顯然不用這兩個指針) chuck 的大小要 align 到8 bytes. 當一個 chunk 處於使用狀態時, 它的下一個 chunk 的 prev_size 域肯定是無效的. 所以實際上, 這個空間也可以被當前 chunk 使用. 這聽起來有點不可思議, 但確實是合理空間複用的例子. 故而實際上, 一個使用中的 chunk 的大小的計算公式應該是:

in_use_size = ( 用戶請求大小 + 8 - 4 ) align to 8 bytes 這裏加8是因爲需要存儲 prev_size 和 size, 但又因爲向下一個 chunk “借”了4個bytes, 所以要減去4. 最後, 因爲空閒的 chunk 和使用中的 chunk 使用的是同一塊空間. 所以肯定要取其中最大者作爲實際的分配空間. 即最終的分配空間 chunk_size = max(in_use_size, 16). 這就是當用戶請求內存分配時, ptmalloc 實際需要分配的內存大小, 在後面的介紹中. 如果不是特別指明的地方, 指的都是這個經過轉換的實際需要分配的內存大小, 而不是用戶請求的內存分配大小.

3. 空閒鏈表bins

當用戶使用free函數釋放掉的內存,ptmalloc並不會馬上交還給操作系統,而是被ptmalloc本身的空閒鏈表bins管理起來了,這樣當下次進程需要malloc一塊內存的時候,ptmalloc就會從空閒的bins上尋找一塊合適大小的內存塊分配給用戶使用。這樣的好處可以避免頻繁的系統調用,降低內存分配的開銷。

malloc將相似大小的chunk用雙向鏈表鏈接起來,這樣一個鏈表被稱爲一個bin。ptmalloc一共維護了128bin。每個bins都維護了大小相近的雙向鏈表的chunk。基於chunk的大小,有下列幾種可用bins:
  1、Fast bin
  2、Unsorted bin
  3、Small bin
  4、Large bin

​ 保存這些bin的數據結構爲:

  fastbinsY:這個數組用以保存fast bins。
  bins:這個數組用以保存unsorted、small以及large bins,共計可容納126個:

  當用戶調用malloc的時候,能很快找到用戶需要分配的內存大小是否在維護的bin上,如果在某一個bin上,就可以通過雙向鏈表去查找合適的chunk內存塊給用戶使用。

a、fast bins

程序在運行時會經常需要申請和釋放一些較小的內存空間。當分配器合併了相鄰的幾個小的 chunk 之後,也許馬上就會有另一個小塊內存的請求,這樣分配器又需要從大的空閒內存中切分出一塊,這樣無疑是比較低效的,故而,malloc 中在分配過程中引入了 fast bins

fast bins是bins的高速緩衝區,大約有10個定長隊列。每個fast bin都記錄着一條free chunk的單鏈表(稱爲binlist ,採用單鏈表是出於fast bin中鏈表中的chunk不會被摘除的特點),增刪chunk都發生在鏈表的前端。 fast bins 記錄着大小以8字節遞增的bin鏈表(??從上面的圖看好像是4字節遞增啊——4字節遞增是因爲指針佔4字節,下面掛的塊的確是8字節遞增)。

當用戶釋放一塊不大於max_fast(默認值64B)的chunk的時候,會默認會被放到fast bins上。當需要給用戶分配的 chunk 小於或等於 max_fast 時,malloc 首先會到fast bins上尋找是否有合適的chunk。(一定大小內的chunk無論是分配還是釋放,都會先在fast bin中過一遍)
除非特定情況,兩個毗連的空閒chunk並不會被合併成一個空閒chunk。不合並可能會導致碎片化問題,但是卻可以大大加速釋放的過程!
分配時,binlist中被檢索的第一個個chunk將被摘除並返回給用戶。free掉的chunk將被添加在索引到的binlist的前端。

b、unsorted bin

  unsorted bin 的隊列使用 bins 數組的第一個,是bins的一個緩衝區,加快分配的速度。當用戶釋放的內存大於max_fast或者fast bins合併後的chunk都會首先進入unsorted bin上。chunk大小 – 無尺寸限制,任何大小chunk都可以添加進這裏。這種途徑給予 ‘glibc malloc’ 第二次機會以重新使用最近free掉的chunk,這樣尋找合適bin的時間開銷就被抹掉了,因此內存的分配和釋放會更快一些。
  用戶malloc時,如果在 fast bins 中沒有找到合適的 chunk,則malloc 會先在 unsorted bin 中查找合適的空閒 chunk,如果沒有合適的bin,ptmalloc會將unsorted bin上的chunk放入bins上,然後到bins上查找合適的空閒chunk。

c、small bins

  大小小於512字節的chunk被稱爲small chunk,而保存small chunks的bin被稱爲small bin。數組從2開始編號,前64個bin爲small bins,small bin每個bin之間相差8個字節,同一個small bin中的chunk具有相同大小。
  每個small bin都包括一個空閒區塊的雙向循環鏈表(也稱binlist)。free掉的chunk添加在鏈表的前端,而所需chunk則從鏈表後端摘除。
  兩個毗連的空閒chunk會被合併成一個空閒chunk。合併消除了碎片化的影響但是減慢了free的速度。
  分配時,當samll bin非空後,相應的bin會摘除binlist中最後一個chunk並返回給用戶。在free一個chunk的時候,檢查其前或其後的chunk是否空閒,若是則合併,也即把它們從所屬的鏈表中摘除併合併成一個新的chunk,新chunk會添加在unsorted bin鏈表的前端。

d、large bins

  大小大於等於512字節的chunk被稱爲large chunk,而保存large chunks的bin被稱爲large bin,位於small bins後面。large bins中的每一個bin分別包含了一個給定範圍內的chunk,其中的chunk按大小遞減排序,大小相同則按照最近使用時間排列。
  兩個毗連的空閒chunk會被合併成一個空閒chunk。
  分配時,遵循原則“smallest-first , best-fit”,從頂部遍歷到底部以找到一個大小最接近用戶需求的chunk。一旦找到,相應adbbchunk就會分成兩塊User chunk(用戶請求大小)返回給用戶。
Remainder chunk 剩餘部分添加到unsorted bin。free時和small bin 類似。

4 三種特殊的chunk

並不是所有chunk都按照上面的方式來組織,有三種列外情況。top chunk,mmaped chunk 和last remainder chunk

a、top chunk

  top chunk相當於分配區的頂部空閒內存(可能就是由brk調用控制的brk指針),當bins上都不能滿足內存分配要求的時候,就會來top chunk上分配。
  當top chunk大小比用戶所請求大小還大的時候,top chunk會分爲兩個部分:User chunk(用戶請求大小)和Remainder chunk(剩餘大小)。其中Remainder chunk成爲新的top chunk。
  當top chunk大小小於用戶所請求的大小時,top chunk就通過sbrk(main arena)或mmap(thread arena)系統調用來擴容。

b、mmaped chunk

  當分配的內存非常大(大於分配閥值,默認128K)的時候,需要被mmap映射,則會放到mmaped chunk上,當釋放mmaped chunk上的內存的時候會直接交還給操作系統。 (chunk中的M標誌位置1)

c、last remainder chunk

  Last remainder chunk是另外一種特殊的chunk,就像top chunk和mmaped chunk一樣,不會在任何bins中找到這種chunk。當需要分配一個small chunk,但在small bins中找不到合適的chunk,如果last remainder chunk的大小大於所需要的small chunk大小,last remainder chunk被分裂成兩個chunk,其中一個chunk返回給用戶,另一個chunk變成新的last remainder chunk。

六、sbrk與mmap

  在堆區中, start_brk 指向 heap 的開始,而 brk 指向 heap 的頂部。可以使用系統調用 brk()和 sbrk()來增 加標識 heap 頂部的 brk 值,從而線性的增加分配給用戶的 heap 空間。在使 malloc 之前,brk 的值等於 start_brk,也就是說 heap 大小爲 0。
  ptmalloc 在開始時,若請求的空間小於 mmap 分配閾值(mmap threshold,默認值爲 128KB)時,主分配區會調用 sbrk()增加一塊大小爲 (128 KB + chunk_size) align 4KB(頁面大小對齊) 的空間作爲 heap。非主分配區會調用 mmap 映射一塊大小爲 HEAP_MAX_SIZE(32 位系統上默認爲 1MB,64 位系統上默認爲 64MB)的空間作爲 sub-heap。這就是前面所說的 ptmalloc 所維護的分配空間;   
  當用戶請求內存分配時,首先會在這個區域內找一塊合適的 chunk 給用戶。當用戶釋放了 heap 中的 chunk 時,ptmalloc 又會使用 fastbins 和 bins 來組織空閒 chunk。以備用戶的下一次分配。
  若需要分配的 chunk 大小小於 mmap分配閾值,而 heap 空間又不夠,則此時主分配區會通過 sbrk()調用來增加 heap 大小,非主分配區會調用 mmap 映射一塊新的 sub-heap,也就是增加 top chunk 的大小,每次 heap 增加的值都會對齊到 4KB。當用戶的請求超過 mmap 分配閾值,並且主分配區使用 sbrk()分配失敗的時候,或是非主分配區在 top chunk 中不能分配到需要的內存時,ptmalloc 會嘗試使用 mmap()直接映射一塊內存到進程內存空間。使用 mmap()直接映射的 chunk 在釋放時直接解除映射,而不再屬於進程的內存空間。任何對該內存的訪問都會產生段錯誤。而在 heap 中或是 sub-heap 中分配的空間則可能會留在進程內存空間內,還可以再次引用(當然是很危險的)。

七、主分配區和非主分配區

內存分配器中,爲了解決多線程鎖爭奪問題,分爲主分配區main_area(分配區的本質就是內存池,管理着chunk,一般用英文area表示)和非主分配區no_main_area。 (主分配區和非主分配區的區別)

 1. 主分配區和非主分配區形成一個環形鏈表進行管理。
 2. 每一個分配區利用互斥鎖使線程對於該分配區的訪問互斥。
 3. 每個進程只有一個主分配區,也可以允許有多個非主分配區。
 4. ptmalloc根據系統對分配區的爭用動態增加分配區的大小,分配區的數量一旦增加,則不會減少。
 5. 主分配區可以使用brk和mmap來分配,而非主分配區只能使用mmap來映射內存塊
 6. 申請小內存時會產生很多內存碎片,ptmalloc在整理時也需要對分配區做加鎖操作。

當一個線程需要使用malloc分配內存的時候,會先查看該線程的私有變量中是否已經存在一個分配區。若是存在。會嘗試對其進行加鎖操作。若是加鎖成功,就在使用該分配區分配內存,若是失敗,就會遍歷循環鏈表中獲取一個未加鎖的分配區。若是整個鏈表中都沒有未加鎖的分配區,則malloc會開闢一個新的分配區,將其加入全局的循環鏈表並加鎖,然後使用該分配區進行內存分配。當釋放這塊內存時,同樣會先獲取待釋放內存塊所在的分配區的鎖。若是有其他線程正在使用該分配區,則必須等待其他線程釋放該分配區互斥鎖之後才能進行釋放內存的操作。

需要注意幾個點:

  • 主分配區通過brk進行分配,非主分配區通過mmap進行分配
  • 從分配區雖然是mmap分配,但是和大於128K直接使用mmap分配沒有任何聯繫。大於128K的內存使用mmap分配,使用完之後直接用ummap還給系統
  • 每個線程在malloc會先獲取一個area,使用area內存池分配自己的內存,這裏存在競爭問題
  • 爲了避免競爭,我們可以使用線程局部存儲,thread cache(tcmalloc中的tc正是此意),線程局部存儲對area的改進原理如下:
    1. 如果需要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量(被稱爲static memory local to a thread 線程局部靜態變量),就需要新的機制來實現。這就是TLS。
    2. thread cache本質上是在static區爲每一個thread開闢一個獨有的空間,因爲獨有,不再有競爭
    3. 每次malloc時,先去線程局部存儲空間中找area,用thread cache中的area分配存在thread area中的chunk。當不夠時纔去找堆區的area
    4. C++11中提供thread_local方便於線程局部存儲
    5. 可以看出主分配區其實是雞肋,實際上tcmalloc和jemalloc都不再使用主分配區,直接使用非主分配區

八、內存分配malloc流程

1、獲取分配區的鎖,防止多線程衝突。(一個進程有一個malloc管理器,而一個進程中的多個線程共享這一個管理器,有競爭,加鎖)

2、計算出實際需要分配的內存的chunk實際大小。

3、判斷chunk的大小,如果小於max_fast(64B),則嘗試去fast bins上取適合的chunk,如果有則分配結束。否則,下一步;

4、判斷chunk大小是否小於512B,如果是,則從small bins上去查找chunk,如果有合適的,則分配結束。否則下一步;

5、ptmalloc首先會遍歷fast bins(注:這裏是第二次遍歷fast bins了,雖然fast bins一般不會合並,但此時會)中的chunk,將相鄰的chunk進行合併,並鏈接到unsorted bin中然後遍歷 unsorted bins。(總體而言,第五部遍歷unsorted bin,只是在遍歷前先合併fast bin,遍歷unsorted bin時一邊遍歷,一邊放到small bin和large bin中)

如果unsorted bins上只有一個chunk並且大於待分配的chunk,則進行切割,並且剩餘的chunk繼續扔回unsorted bins;
如果unsorted bins上有大小和待分配chunk相等的,則返回,並從unsorted bins刪除;
如果unsorted bins中的某一chunk大小 屬於small bins的範圍,則放入small bins的頭部;
如果unsorted bins中的某一chunk大小 屬於large bins的範圍,則找到合適的位置放入。若未分配成功,轉入下一步;
6、從large bins中查找找到合適的chunk之後,然後進行切割,一部分分配給用戶,剩下的放入unsorted bin中。

7、如果搜索fast bins和bins都沒有找到合適的chunk,那麼就需要操作top chunk來進行分配了 。當top chunk大小比用戶所請求大小還大的時候,top chunk會分爲兩個部分:User chunk(用戶請求大小)和Remainder chunk(剩餘大小)。其中Remainder chunk成爲新的top chunk。 當top chunk大小小於用戶所請求的大小時,top chunk就通過sbrk(main arena)或mmap(thread arena)系統調用來擴容。

8、到了這一步,說明 top chunk 也不能滿足分配要求,所以,於是就有了兩個選擇: 如 果是主分配區,調用 sbrk(),增加 top chunk 大小;如果是非主分配區,調用 mmap 來分配一個新的 sub-heap,增加 top chunk 大小;或者使用 mmap()來直接分配。在 這裏,需要依靠 chunk 的大小來決定到底使用哪種方法。判斷所需分配的 chunk 大小是否大於等於 mmap 分配閾值,如果是的話,則轉下一步,調用 mmap 分配, 否則跳到第 10 步,增加 top chunk 的大小。

9、使用 mmap 系統調用爲程序的內存空間映射一塊 chunk_size align 4kB 大小的空間。 然後將內存指針返回給用戶。

10、判斷是否爲第一次調用 malloc,若是主分配區,則需要進行一次初始化工作,分配 一塊大小爲(chunk_size + 128KB) align 4KB 大小的空間作爲初始的 heap。若已經初 始化過了,主分配區則調用 sbrk()增加 heap 空間,分主分配區則在 top chunk 中切 割出一個 chunk,使之滿足分配需求,並將內存指針返回給用戶。

九、內存回收流程

  1. 獲取分配區的鎖,保證線程安全。
  2. 如果free的是空指針,則返回,什麼都不做。
  3. 判斷當前chunk是否是mmap映射區域映射的內存,如果是,則直接munmap()釋放這塊內存。前面的已使用chunk的數據結構中,我們可以看到有M來標識是否是mmap映射的內存。
  4. 判斷chunk是否與top chunk相鄰,如果相鄰,則直接和top chunk合併(和top chunk相鄰相當於和分配區中的空閒內存塊相鄰)。轉到步驟8
  5. 如果chunk的大小大於max_fast(64b),則放入unsorted bin,並且檢查是否有合併,有合併情況並且和top chunk相鄰,則轉到步驟8;沒有合併情況則free。
  6. 如果chunk的大小小於 max_fast(64b),則直接放入fast bin,fast bin並沒有改變chunk的狀態。沒有合併情況,則free;有合併情況,轉到步驟7
  7. 在fast bin,如果當前chunk的下一個chunk也是空閒的,則將這兩個chunk合併,放入unsorted bin上面。合併後的大小如果大於64B,會觸發進行fast bins的合併操作,fast bins中的chunk將被遍歷,並與相鄰的空閒chunk進行合併,合併後的chunk會被放到unsorted bin中,fast bin會變爲空。合併後的chunk和topchunk相鄰,則會合併到topchunk中。轉到步驟8
  8. 判斷top chunk的大小是否大於mmap收縮閾值(默認爲128KB),如果是的話,對於主分配區,則會試圖歸還top chunk中的一部分給操作系統。free結束。

十、使用注意事項

爲了避免Glibc內存暴增,需要注意:

  1. 後分配的內存先釋放,因爲ptmalloc收縮內存是從top chunk開始,如果與top chunk相鄰的chunk不能釋放,top chunk以下的chunk都無法釋放。
  2. Ptmalloc不適合用於管理長生命週期的內存,特別是持續不定期分配和釋放長生命週期的內存,這將導致ptmalloc內存暴增。
  3. 不要關閉 ptmalloc 的 mmap 分配閾值動態調整機制,因爲這種機制保證了短生命週期的 內存分配儘量從 ptmalloc 緩存的內存 chunk 中分配,更高效,浪費更少的內存。
  4. 多線程分階段執行的程序不適合用ptmalloc,這種程序的內存更適合用內存池管理 (因爲同一個進程下的多線程要加鎖後才能使用malloc分配器)
  5. 儘量減少程序的線程數量和避免頻繁分配/釋放內存。頻繁分配,會導致鎖的競爭,最終導致非主分配區增加,內存碎片增高,並且性能降低。
  6. 防止內存泄露,ptmalloc對內存泄露是相當敏感的,根據它的內存收縮機制,如果與top chunk相鄰的那個chunk沒有回收,將導致top chunk一下很多的空閒內存都無法返回給操作系統。
  7. 防止程序分配過多的內存,或是由於glibc內存暴增,導致系統內存耗盡,程序因爲OOM被系統殺掉。預估程序可以使用的最大物理內存的大小,配置系統的/proc/sys/vm/overcommit_memory ,/proc/sys/vm/overcommit_ratio,以及使用ulimit -v限制程序能使用的虛擬內存的大小,防止程序因OOM被殺死掉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章