malloc的實現原理

2021-09-25 17:48:49 字數 2697 閱讀 8233

malloc是c語言最常用的標準庫函式之一,用於在程式執行中動態地申請記憶體空間。我們都會使用它,其函式原型為:

extern void *malloc(unsigned int num_bytes);

那麼它是怎麼實現的呢?不同的編譯環境中對它的實現可能不同。比如glibc(the gnu c library)就有自己對malloc庫函式的實現方法,並且是開源的。如果讓我們自己實現malloc功能的函式,該怎麼寫?下面學習一下malloc實現的原理。

首先,我們需要知道,作業系統核心是怎麼把一段記憶體分配給程序的?這當然需要系統呼叫了。使用者態申請分配內容的系統呼叫是sbrk(n),引數n是期望得到的記憶體位元組數。但是,頻繁的呼叫sbrk進行分配會使得真實記憶體出現越來越多的零碎空間。linux作業系統的基本分配方式是夥伴分配方式,所以即使申請的位元組數不是2的冪數,系統也會分配出2的冪數的空間來。因此malloc的實現不會是每次都動用核心的。

malloc採用的總體策略是:

先系統呼叫sbrk一次,會得到一段較大的並且是連續的空間。程序把系統核心分配給自己的這段空間留著慢慢用。之後呼叫malloc時就從這段空間中分配,free**時就再還回來(而不是還給系統核心)。只有當這段空間全部被分配掉時還不夠用時,才再次系統呼叫sbrk。當然,這一次呼叫sbrk後核心分配給程序的空間和剛才的那塊空間一般不會是相鄰的。

malloc如何使用得到動態記憶體空間?一次sbrk之後,malloc就會保留著一段大的連續空間(稱作堆空間)。之後對於堆空間malloc不斷地分配,free不斷地收回,這段空間裡的格局必定是「亂七八糟」,有的已分配,有的未分配。

實現動態記憶體分配往往要考慮以下四個問題:

(1)空閒塊組織:我們如何記錄空閒塊?

(2)選擇:我們如何選擇乙個合適的空閒塊來作為乙個新分配的塊?

(3)分割:在我們選了乙個空閒塊分配出我們需要的大小之後,我們如何處理這個空閒塊中的剩餘部分?

(4)合併:我們如何處理乙個剛剛被釋放的塊?

malloc的空閒鍊錶機制:這是對問題(1)的解答。有兩種方法:顯式空閒鍊錶、隱式空閒鍊錶。

(1)顯式空閒鍊錶:用乙個鍊錶將可用的記憶體塊連線為乙個長長的列表,稱為空閒鍊錶。將堆組織成雙向空閒鍊錶,每乙個空閒塊結點都包含乙個祖先指標和乙個後繼指標。鍊錶中的每個結點記錄乙個連續的、未分配的記憶體小塊。結點的結構很簡單,只需要記錄可用記憶體小塊的首位址和大小即可。

(2)隱式空閒鍊錶:隱式空閒鍊錶由n個塊組成,乙個塊由頭部、有效載荷(只包括已分配的塊),以及可能的一些額外的填充(為了保證記憶體位元組對齊而需要的填充)和尾部組成。頭部大小為4個位元組,在強加雙字對齊的約束之後,塊大小總是8的倍數,所以用高29位來表示儲存塊大小,剩餘3位中利用最低位來表示塊是否已分配(1表示已分配,0表示空閒);尾部和頭部的內容一樣,加入尾部是為了分配器可以判斷出乙個塊的起始位置和狀態。整個堆空間就是乙個隱式空閒鍊錶,從低位址向高位址生長,第乙個和最後乙個8位元組標記為已分配,以確定堆的大小。

空閒鍊錶如何從中選擇分配記憶體塊?這是對問題(2)的解答。有下面四種選擇方法。

(1)首次適應法(first fit):鍊錶按塊位址排序。選擇第乙個滿足要求的空閒塊。特點:低位址碎片多,高位址碎片少。

(2)最佳適應法(best fit):鍊錶按空閒塊大小排序。選擇滿足要求的,且大小最小的空閒塊。特點:費時間,並且會出現很小的碎片。

(3)最壞適應法(worst fit):鍊錶按空閒塊大小排序。選擇最大的空閒塊。特點:碎片少,容易缺乏大塊。

(4)迴圈首次適應法(next fit):鍊錶按塊位址排序。從上次分配位置開始找到第乙個滿足要求的空閒塊。特點:碎片分布的又多又均勻。

如何處理被選空閒塊中的剩餘部分?這是對問題(3)的解答。一般來講,是要把剩餘的部分再插入回到空閒鍊錶中去的。要注意乙個空閒塊分割成兩個塊時,需要騰出若干位元組作為塊的頭部尾部等部分。

如何合併被釋放的塊?這是對問題(4)的解答。有兩種方法:立即合併、推遲合併。對於隱式空閒鍊錶,合併的具體過程是,

(1)前後塊都已分配:直接釋放當前塊即可;

(2)前塊分配、後塊空閒:和後塊合併;

(3)前塊空閒、後塊分配:和前塊合併;

(4)前後塊都已空閒:和前後塊合併;

glibc對malloc的實現

目前最新版本為2.18,glibc原始碼目錄/glibc-2.18/malloc中可以看到。在glibc的malloc的實現中, 分配虛存有兩種系統呼叫可用: brk()和mmap(), 如果要分配大塊記憶體, glibc會使用mmap()去分配記憶體,這種記憶體靠近棧。

基於unix 的系統有兩個可對映到附加記憶體中的基本系統呼叫:

brk: brk() 是乙個非常簡單的系統呼叫。還記得系統中斷點嗎?該位置是程序對映的記憶體邊界。 brk()只是簡單地將這個位置向前或者向後移動,就可以向程序新增記憶體或者從程序取走記憶體。sbrk()是以增量的方式增加擴大記憶體。

mmap: mmap(),或者說是「記憶體映像」,類似於 brk(),但是更為靈活。首先,它可以對映任何位置的記憶體,而不單單只侷限於程序。其次,它不僅可以將虛擬位址對映到物理的 ram 或者 swap,它還可以將它們對映到檔案和檔案位置,這樣,讀寫記憶體將對檔案中的資料進行讀寫。不過在這裡,我們只關心 mmap向程序新增被對映的記憶體的能力。

在glibc的malloc的實現有乙個優化:

當malloc()一塊很小的記憶體是, glibc呼叫brk(), 只需要在heap中移動一下指標, 即可獲得可用虛存, 這樣分配得到的位址較小.

當malloc()一塊較大記憶體時, glibc呼叫mmap(), 需要在核心中重新分配vma結構等, 他會在靠近棧的地方分配虛存, 這樣返回的位址大.

malloc的實現原理

malloc 是 c語言中動態 儲存管理 的一組標準庫函式之一。其作用是在記憶體的動態儲存區中分配乙個長度為size的連續空間。其引數是乙個無符號整形數,返回值是乙個指向所分配的連續儲存域的起始位址的指標。動態記憶體分配 就 是指在程式執行的過程中動態地分配或者 儲存空間的分配記憶體的方法。動態記憶...

malloc實現原理

malloc 是c語言中動態儲存管理 的一組標準庫函式之一。其作用是在記憶體的動態儲存區中分配乙個長度為size的連續空間。其引數是乙個無符號整形數,返回值 是乙個指向所分配的連續儲存域的起始位址的指標。動態記憶體分配 就 是指在程式執行的過程中動態地分配或者 儲存空間的分配記憶體的方法。動態記憶體...

malloc實現原理簡介

malloc 是c語言中動態儲存管理 的一組標準庫函式之一。其作用是在記憶體的動態儲存區中分配乙個長度為size的連續空間。其引數是乙個無符號整形數,返回值 是乙個指向所分配的連續儲存域的起始位址的指標。動態記憶體分配 就 是指在程式執行的過程中動態地分配或者 儲存空間的分配記憶體的方法。動態記憶體...