淺析malloc()的幾種實現方式

2puT發表於2016-07-19
malloc()是C語言中動態儲存管理的一組標準庫函式之一。其作用是在記憶體的動態儲存區中分配一個長度為size的連續空間。其引數是一個無符號整形數,返回值是一個指向所分配的連續儲存域的起始地址的指標。


   動態記憶體分配就是指在程式執行的過程中動態地分配或者回收儲存空間的分配記憶體的方法。動態記憶體分配不像陣列等靜態記憶體分配方法那樣需要預先分配儲存空間,而是由系統根據程式的需要即時分配,且分配的大小就是程式要求的大小。本文簡單介紹動態記憶體分配函式malloc()及幾種實現方法。


   1. 簡介


  malloc()是C語言中動態儲存管理的一組標準庫函式之一。其作用是在記憶體的動態儲存區中分配一個長度為size的連續空間。其引數是一個無符號整形數,返回值是一個指向所分配的連續儲存域的起始地址的指標。還有一點必須注意的是,當函式未能成功分配儲存空間(如記憶體不足)就會返回一個NULL指標。所以在呼叫該函式時應該檢測返回值是否為NULL並執行相應的操作。


   2. 函式說明


  C語言的動態儲存管理由一組標準庫函式實現,其原型在標準檔案<stdlib.h>裡描述,需要用這些功能時應包含這個檔案。與動態儲存分配有關的函式共有四個,其中就包括儲存分配函式malloc()。函式原型是:void *malloc (size_t n);這裡的size_t是標準庫裡定義的一個型別,它是一個無符號整型。這個整型能夠滿足所有對儲存塊大小描述的需要,具體相當於哪個整型由具體的C系統確定。malloc的返回值為(void *)型別(這是通用指標的一個重要用途),它分配一片能存放大小為n的資料的儲存塊,返回對應的指標值;如果不能滿足申請(找不到能滿足要求的儲存塊)就返回NULL。在使用時,應該把malloc的返回值轉換到特定指標型別,賦給一個指標。


    注意,雖然這裡的儲存塊是通過動態分配得到的,但是它的大小也是確定的,同樣不允許越界使用。例如上面程式段分配的塊裡能存n個雙精度資料,隨後的使用就必須在這個範圍內進行。越界使用動態分配的儲存塊,尤其是越界賦值,可能引起非常嚴重的後果,通常會破壞程式的執行系統,可能造成本程式或者整個計算機系統垮臺。


  下例是一個動態分配的例子:
#include <stdlib.h>


main()
{
 int count,*array; /*count是一個計數器,array是一個整型指標,也可以理解為指向一個整型陣列的首地址*/
 if((array(int *) malloc (10*sizeof(int)))==NULL)
 {
  printf("不能成功分配儲存空間。");
  exit(1);
 }
 for (count=0;count〈10;count++) /*給陣列賦值*/
  array[count]=count;
 for(count=0;count〈10;count++) /*列印陣列元素*/
  printf("%2d",array[count]);
}


  上例中動態分配了10個整型儲存區域,然後進行賦值並列印。例中if((array(int *) malloc (10*sizeof(int)))==NULL)語句可以分為以下幾步:
  1)分配10個整型的連續儲存空間,並返回一個指向其起始地址的整型指標
  2)把此整型指標地址賦給array
  3)檢測返回值是否為NULL


   3. malloc()工作機制


  malloc函式的實質體現在,它有一個將可用的記憶體塊連線為一個長長的列表的所謂空閒連結串列。呼叫malloc函式時,它沿連線表尋找一個大到足以滿足使用者請求所需要的記憶體塊。然後,將該記憶體塊一分為二(一塊的大小與使用者請求的大小相等,另一塊的大小就是剩下的位元組)。接下來,將分配給使用者的那塊記憶體傳給使用者,並將剩下的那塊(如果有的話)返回到連線表上。呼叫free函式時,它將使用者釋放的記憶體塊連線到空閒鏈上。到最後,空閒鏈會被切成很多的小記憶體片段,如果這時使用者申請一個大的記憶體片段,那麼空閒鏈上可能沒有可以滿足使用者要求的片段了。於是,malloc函式請求延時,並開始在空閒鏈上翻箱倒櫃地檢查各記憶體片段,對它們進行整理,將相鄰的小空閒塊合併成較大的記憶體塊。


   4. malloc()在作業系統中的實現


  在 C 程式中,多次使用malloc () 和 free()。不過,您可能沒有用一些時間去思考它們在您的作業系統中是如何實現的。本節將向您展示 malloc 和 free 的一個最簡化實現的程式碼,來幫助說明管理記憶體時都涉及到了哪些事情。


  在大部分作業系統中,記憶體分配由以下兩個簡單的函式來處理:


  void *malloc (long numbytes):該函式負責分配 numbytes 大小的記憶體,並返回指向第一個位元組的指標。


  void free(void *firstbyte):如果給定一個由先前的 malloc 返回的指標,那麼該函式會將分配的空間歸還給程式的“空閒空間”。


  malloc_init 將是初始化記憶體分配程式的函式。它要完成以下三件事:將分配程式標識為已經初始化,找到系統中最後一個有效記憶體地址,然後建立起指向我們管理的記憶體的指標。這三個變數都是全域性變數:


  清單 1. 我們的簡單分配程式的全域性變數


        int has_initialized = 0;
        void *managed_memory_start;
        void *last_valid_address;




  如前所述,被對映的記憶體的邊界(最後一個有效地址)常被稱為系統中斷點或者 當前中斷點。在很多 UNIX? 系統中,為了指出當前系統中斷點,必須使用 sbrk(0) 函式。 sbrk 根據引數中給出的位元組數移動當前系統中斷點,然後返回新的系統中斷點。使用引數 0 只是返回當前中斷點。這裡是我們的 malloc 初始化程式碼,它將找到當前中斷點並初始化我們的變數:




  清單 2. 分配程式初始化函式
/* Include the sbrk function */
 
#include 
void malloc_init()
{
/* grab the last valid address from the OS */
last_valid_address = sbrk(0);
/* we don't have any memory to manage yet, so
 *just set the beginning to be last_valid_address
 */
managed_memory_start = last_valid_address;
/* Okay, we're initialized and ready to go */
 has_initialized = 1;
}




  現在,為了完全地管理記憶體,我們需要能夠追蹤要分配和回收哪些記憶體。在對記憶體塊進行了 free 呼叫之後,我們需要做的是諸如將它們標記為未被使用的等事情,並且,在呼叫 malloc 時,我們要能夠定位未被使用的記憶體塊。因此, malloc 返回的每塊記憶體的起始處首先要有這個結構:




  清單 3. 記憶體控制塊結構定義
struct mem_control_block {
int is_available;
int size;
};
  


   現在,您可能會認為當程式呼叫 malloc 時這會引發問題 —— 它們如何知道這個結構?答案是它們不必知道;在返回指標之前,我們會將其移動到這個結構之後,把它隱藏起來。這使得返回的指標指向沒有用於任何其他用途的記憶體。那樣,從呼叫程式的角度來看,它們所得到的全部是空閒的、開放的記憶體。然後,當通過 free() 將該指標傳遞回來時,我們只需要倒退幾個記憶體位元組就可以再次找到這個結構。




  在討論分配記憶體之前,我們將先討論釋放,因為它更簡單。為了釋放記憶體,我們必須要做的惟一一件事情就是,獲得我們給出的指標,回退 sizeof(struct mem_control_block) 個位元組,並將其標記為可用的。這裡是對應的程式碼:




  清單 4. 解除分配函式
void free(void *firstbyte) {
struct mem_control_block *mcb;
/* Backup from the given pointer to find the
 * mem_control_block
 */
mcb = firstbyte - sizeof(struct mem_control_block);
/* Mark the block as being available */
mcb->is_available = 1;
/* That's It!  We're done. */
return;
}




  如您所見,在這個分配程式中,記憶體的釋放使用了一個非常簡單的機制,在固定時間內完成記憶體釋放。分配記憶體稍微困難一些。以下是該演算法的略述:




  清單 5. 主分配程式的虛擬碼
1. If our allocator has not been initialized, initialize it.
2. Add sizeof(struct mem_control_block) to the size requested.
3. start at managed_memory_start.
4. Are we at last_valid address?
5. If we are:
   A. We didn't find any existing space that was large enough
      -- ask the operating system for more and return that.
6. Otherwise:
   A. Is the current space available (check is_available from
      the mem_control_block)?
   B. If it is:
      i)   Is it large enough (check "size" from the
           mem_control_block)?
      ii)  If so:
           a. Mark it as unavailable
           b. Move past mem_control_block and return the
              pointer
      iii) Otherwise:
           a. Move forward "size" bytes
           b. Go back go step 4
   C. Otherwise:
      i)   Move forward "size" bytes
      ii)  Go back to step 4




  我們主要使用連線的指標遍歷記憶體來尋找開放的記憶體塊。這裡是程式碼:




  清單 6. 主分配程式
void *malloc(long numbytes) {
/* Holds where we are looking in memory */
void *current_location;
/* This is the same as current_location, but cast to a
 * memory_control_block
 */
struct mem_control_block *current_location_mcb;
/* This is the memory location we will return.  It will
 * be set to 0 until we find something suitable
 */
void *memory_location;
/* Initialize if we haven't already done so */
if(! has_initialized) {
malloc_init();
}
/* The memory we search for has to include the memory
 * control block, but the users of malloc don't need
 * to know this, so we'll just add it in for them.
 */
numbytes = numbytes + sizeof(struct mem_control_block);
/* Set memory_location to 0 until we find a suitable
 * location
 */
memory_location = 0;
/* Begin searching at the start of managed memory */
current_location = managed_memory_start;
/* Keep going until we have searched all allocated space */
while(current_location != last_valid_address)
{
/* current_location and current_location_mcb point
 * to the same address.  However, current_location_mcb
 * is of the correct type, so we can use it as a struct.
 * current_location is a void pointer so we can use it
 * to calculate addresses.
 */
current_location_mcb =
(struct mem_control_block *)current_location;
if(current_location_mcb->is_available)
{
if(current_location_mcb->size >= numbytes)
{
/* Woohoo!  We've found an open,
 * appropriately-size location.
 */
/* It is no longer available */
current_location_mcb->is_available = 0;
/* We own it */
memory_location = current_location;
/* Leave the loop */
break;
}
}
/* If we made it here, it's because the Current memory
 * block not suitable; move to the next one
 */
current_location = current_location +
current_location_mcb->size;
}
/* If we still don't have a valid location, we'll
 * have to ask the operating system for more memory
 */
if(! memory_location)
{
/* Move the program break numbytes further */
sbrk(numbytes);
/* The new memory will be where the last valid
 * address left off
 */
memory_location = last_valid_address;
/* We'll move the last valid address forward
 * numbytes
 */
last_valid_address = last_valid_address + numbytes;
/* We need to initialize the mem_control_block */
current_location_mcb = memory_location;
current_location_mcb->is_available = 0;
current_location_mcb->size = numbytes;
}
/* Now, no matter what (well, except for error conditions),
 * memory_location has the address of the memory, including
 * the mem_control_block
 */
/* Move the pointer past the mem_control_block */
memory_location = memory_location + sizeof(struct mem_control_block);
/* Return the pointer */
return memory_location;
 }




  這就是我們的記憶體管理器。現在,我們只需要構建它,並在程式中使用它即可。
 


  5. malloc()的其他實現


  malloc() 的實現有很多,這些實現各有優點與缺點。在設計一個分配程式時,要面臨許多需要折衷的選擇,其中包括:


分配的速度。 
回收的速度。 
有執行緒的環境的行為。 
記憶體將要被用光時的行為。 
區域性快取。 
簿記(Bookkeeping)記憶體開銷。 
虛擬記憶體環境中的行為。 
小的或者大的物件。 
實時保證。
每一個實現都有其自身的優缺點集合。在我們的簡單的分配程式中,分配非常慢,而回收非常快。另外,由於它在使用虛擬記憶體系統方面較差,所以它最適於處理大的物件。


  還有其他許多分配程式可以使用。其中包括:


  Doug Lea Malloc:Doug Lea Malloc 實際上是完整的一組分配程式,其中包括 Doug Lea 的原始分配程式,GNU libc 分配程式和 ptmalloc。 Doug Lea 的分配程式有著與我們的版本非常類似的基本結構,但是它加入了索引,這使得搜尋速度更快,並且可以將多個沒有被使用的塊組合為一個大的塊。它還支援快取,以便更快地再次使用最近釋放的記憶體。 ptmalloc 是 Doug Lea Malloc 的一個擴充套件版本,支援多執行緒。在本文後面的 參考資料部分中,有一篇描述 Doug Lea 的 Malloc 實現的文章。




  BSD Malloc:BSD Malloc 是隨 4.2 BSD 發行的實現,包含在 FreeBSD 之中,這個分配程式可以從預先確實大小的物件構成的池中分配物件。它有一些用於物件大小的 size 類,這些物件的大小為 2 的若干次冪減去某一常數。所以,如果您請求給定大小的一個物件,它就簡單地分配一個與之匹配的 size 類。這樣就提供了一個快速的實現,但是可能會浪費記憶體。在 參考資料部分中,有一篇描述該實現的文章。




  Hoard:編寫 Hoard 的目標是使記憶體分配在多執行緒環境中進行得非常快。因此,它的構造以鎖的使用為中心,從而使所有程式不必等待分配記憶體。它可以顯著地加快那些進行很多分配和回收的多執行緒程式的速度。在 參考資料部分中,有一篇描述該實現的文章。
眾多可用的分配程式中最有名的就是上述這些分配程式。如果您的程式有特別的分配需求,那麼您可能更願意編寫一個定製的能匹配您的程式記憶體分配方式的分配程式。不過,如果不熟悉分配程式的設計,那麼定製分配程式通常會帶來比它們解決的問題更多的問題。


   6. 結束語


  前面已經提過,多次呼叫malloc()後空閒記憶體被切成很多的小記憶體片段,這就使得使用者在申請記憶體使用時,由於找不到足夠大的記憶體空間,malloc()需要進行記憶體整理,使得函式的效能越來越低。聰明的程式設計師通過總是分配大小為2的冪的記憶體塊,而最大限度地降低潛在的malloc效能喪失。也就是說,所分配的記憶體塊大小為4位元組、8位元組、16位元組、18446744073709551616位元組,等等。這樣做最大限度地減少了進入空閒鏈的怪異片段(各種尺寸的小片段都有)的數量。儘管看起來這好像浪費了空間,但也容易看出浪費的空間永遠不會超過50%。


   參考文獻:


  [1]  Jonathan Bartlett,記憶體管理內幕. developerWorks 中國,2004年11月


  [2]  Jan Lindblad,記憶體碎片處理技術. EDN電子設計技術,2004年10月08日

相關文章