記憶體池原理大揭秘

2021-08-31 20:59:14 字數 4076 閱讀 6076

本文由[amc](發表於雲+社群專欄

在 c 語言的動態申請記憶體技術中,相比起alloc/free系統呼叫,記憶體池(memory pool)是與現在系統中請求一大片連續的記憶體空間,然後在執行時根據實際需要分配出去的技術。使用記憶體池的優點有:

速度遠比malloc/free快,因為減少了系統呼叫的次數,特別是頻繁申請/釋放記憶體塊的情況

避免了頻繁申請/釋放記憶體之後,系統的大量記憶體碎片

節省空間

根據分配出去的記憶體大小,記憶體池可以分為兩類:

每次分配出去的記憶體單元(稱為unit或者cell)的大小為程式預先定義的值。釋放記憶體塊時,則只需要簡單地掛回記憶體池煉表中即可。又稱為 「固定尺寸緩衝池」。

常規的做法是:將不同 unit size 的記憶體池整合在一起,以滿足不同記憶體塊大小的使用需求

不分配固定長度,記憶體的分配只是在一大塊空閒的記憶體上滑動。優點是分配效率很高,缺點是成批地**記憶體,因為釋放的記憶體無法直接重複利用。

使用這種需要合理規劃每塊記憶體的管理區域,所以又叫做 「基於區域的」 記憶體管理。使用這種做法的分配器,舉例有apache portable runtime中的 apr_pool 工具。本文不討論這種記憶體池。

定長記憶體池有一些基本和必要的概念,需要定義在記憶體池的結構資料中。以下命名方式使用變體的匈牙利命名法,比如nnextn表示變數型別為整形。類似地,p表示指標。

每次程式呼叫mempool_alloc獲取乙個記憶體區域後,會獲得一塊連續的記憶體區域。管理乙個這樣的記憶體區域的單元就成為記憶體單元 unit,有時也稱作chunk。每個 unit 需要包含以下資料:

pdata:實際的記憶體區域,其大小在建立時由呼叫方指定

乙個記憶體塊,記憶體塊中儲存著一系列的記憶體單元。

這個資料結構需要包含以下基本資訊:

nsize:整型資料,表示該 block 在記憶體中的大小

nfree:整型,表示剩下有幾個 unit 未被分配

乙個記憶體池總的管理資料結構,換句話說,是乙個記憶體池物件。

pblock:指標,指向第乙個 memory block

nunitsize:整型,表示每個 unit 的尺寸

ninitsize:整型,表示第乙個 block 的 unit 個數

ngrowsize:整型,表示在第乙個 block 之外再繼續增加的每個 block 的 unit 個數

建立乙個 memory pool,必須的引數為 unit size,可選引數為上文 memory pool 的ninitsizengrowsize

銷毀整個 memory pool 並交還給作業系統。

從 memory pool 中分配乙個 unit,其尺寸是預先定義的 unit size。

釋放乙個指定的 unit。

現在我們用乙個 unit size 為 1024、init size 為 4(每乙個 block 有 4 個 units)的 memory pool 為例,解釋一下記憶體池的工作原理。下文假設整型的寬度為 4 個位元組。

程式開始,呼叫並建立乙個 memory pool。此時呼叫的函式為mempoolcreate(),程式會建立乙個資料結構,相應的結構體成員及其取值如下:

當呼叫者第一次請求mempoolalloc()時,記憶體池發現 block 鍊錶為空,於是想系統申請記憶體,建立 memory block,並初始化如下(其中位址值為假設值):

其中nsize = 4112 = sizeof(mempool) + ninitsize * sizeof(memunit)。每乙個nnext依次加一,各指代著跟著自己的下乙個 unit。最後乙個 unit 的nnext值無意義,因此不說明其取值。

然後返回需要的 unit 中的記憶體。返回記憶體的邏輯如下:

記憶體池在 block 中查詢nfree成員

由於nfree > 0,表示有未分配的 unit,因此繼續在該 block 中檢視nfirst成員

nfirst等於 0,表示該 block 中位置為 0 的 unit 可用。因此記憶體池可以將這個 unit 中的pdata位址返回給呼叫方。pdata的位址值計算方式為:pblock + sizeof(memblock) + nfirst * (sizeof(memunit)) + sizeof(nnext) = 0x10010nfree減一

修改nfirst的值,標記下乙個可用的 unit。注意這裡的nfirst切切不能簡單地加一,而是取返回給呼叫方的 unit 所對應的nnext的值,也就是下圖(2)處原來的值1pdata的位址值返回。為便於說明,這塊區域我們標記為 ca

操作後各資料結構的狀態如下:

第二次呼叫alloc的情況類似。呼叫後各資料結構的狀態如下:

我們先看看結果:

首先程式會檢查 ca 的位址值,很快就會發現,位址 0x10010 位於上述第乙個 block 的範圍之內(0x10000 <= 0x10010 <= (0x10000 + 4112))。再計算偏移值可以很快得出其對應的nnext標號,也就是上圖中的(2)位置。

** unit,此時需要標記相應的成員值以標示 unit 的**狀態。首先檢視nfirst的值,參見上前幅圖,nfirst的值為 3,表示位置(3)處的 unit 是可用的。因此我們首先把(2)處的nnext值設定為 3,將其加回到可用 unit 的鍊錶中

nfirst的值修改為0,也就是代表剛剛**回來的 unit 的標號,而(2)處的值賦值為 2,表示b(3)的 unit

其實可以看到,上面就是乙個簡單的鍊錶操作。根據上面的過程,如果 cb 也釋放了的話,那麼 memory pool 的狀態則會變成這樣:

到這個時候,由於整個 block 已經完全**了(nfree == ninitsize),那麼根據不同的策略,可以考慮將整個 block 從記憶體中釋放掉。

我們回到alloc的邏輯中,可以看到記憶體池最開始會檢查 block 的nfree成員。如果nfree == 0的時候,那麼就會在該 block 的pnext中去找到下乙個 block,再去檢查nfree。如果發現 block 鍊錶已經結束了,那就意味著當前所有的 block 已滿,必須建立新的 block。

在實際設計中,我們需要考慮選取合適的 init size 和 grow size 值。從上面的演算法中可以看到,如果alloc/free呼叫非常頻繁時,第乙個 block 的使用效率是非常高的。

有些簡化的版本中,可以不使用pnext來維護鍊錶,也就是只有乙個 block,並且記憶體的使用有乙個明確且受控的上限值。這經常用在沒有malloc系統呼叫的 rtos 或者是一些對記憶體非常敏感的嵌入式系統中。

如果要用於多執行緒環境中,那麼 memory pool 結構體需要加上鎖

海量技術實踐經驗,盡在雲加社群!

記憶體池原理大揭秘

本文由 amc 雲 社群專欄 在 c 語言的動態申請記憶體技術中,相比起alloc free系統呼叫,記憶體池 memory pool 是與現在系統中請求一大片連續的記憶體空間,然後在執行時根據實際需要分配出去的技術。使用記憶體池的優點有 速度遠比malloc free快,因為減少了系統呼叫的次數,...

新手必看,爬蟲工作原理大揭秘

大資料時代下,資料採集推動著資料分析,資料分析推動發展。但是在這個過程中會出現很多問題。拿最簡單最基礎的爬蟲採集資料為例,過程中就會面臨,ip被封,爬取受限 違法操作等多種問題,所以在爬去資料之前,一定要了解好預爬 是否涉及違法操作,找到合適的 ip訪問 等一系列問題。當然在真正去運用之前,我們應該...

記憶體池的原理及實現

在軟體開發中,有些物件使用非常頻繁,那麼我們可以預先在堆中例項化一些物件,我們把維護這些物件的結構叫 記憶體池 在需要用的時候,直接從記憶體池中拿,而不用從新例項化,在要銷毀的時候,不是直接free delete,而是返還給記憶體池。把那些常用的物件存在記憶體池中,就不用頻繁的分配 記憶體,可以相對...