歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐干貨哦~
本文由[amc](https://cloud.tencent.com/developer/user/1024461?fromSource=waitui)發表於雲+社區專欄
在 C 語言的動態申請內存技術中,相比起 alloc/free 系統調用,內存池(memory pool)是與現在系統中請求一大片連續的內存空間,然后在運行時根據實際需要分配出去的技術。使用內存池的優點有:
- 速度遠比
malloc/free快,因為減少了系統調用的次數,特別是頻繁申請/釋放內存塊的情況 - 避免了頻繁申請/釋放內存之后,系統的大量內存碎片
- 節省空間
分類
根據分配出去的內存大小,內存池可以分為兩類:
Fixed-size Allocation
每次分配出去的內存單元(稱為 unit 或者 cell)的大小為程序預先定義的值。釋放內存塊時,則只需要簡單地掛回內存池鏈表中即可。又稱為 “固定尺寸緩沖池”。
常規的做法是:將不同 unit size 的內存池整合在一起,以滿足不同內存塊大小的使用需求
Variable-size allocation
不分配固定長度,內存的分配只是在一大塊空閑的內存上滑動。優點是分配效率很高,缺點是成批地回收內存,因為釋放的內存無法直接重復利用。
使用這種需要合理規划每塊內存的管理區域,所以又叫做 “基於區域的” 內存管理。使用這種做法的分配器,舉例有 Apache Portable Runtime 中的 apr_pool 工具。本文不討論這種內存池。
原理和結構
概念和數據結構
定長內存池有一些基本和必要的概念,需要定義在內存池的結構數據中。以下命名方式使用變體的匈牙利命名法,比如 nNext,n表示變量類型為整形。類似地,p表示指針。
Memory Unit
每次程序調用 MemPool_Alloc 獲取一個內存區域后,會獲得一塊連續的內存區域。管理一個這樣的內存區域的單元就成為內存單元 unit,有時也稱作 chunk。每個 unit 需要包含以下數據:
nNext:整型數據,表示下一個可供分配的 unit 的標識號。功能請參見后問pData[]:實際的內存區域,其大小在創建時由調用方指定
Memory Block
一個內存塊,內存塊中保存着一系列的內存單元。
這個數據結構需要包含以下基本信息:
nSize:整型數據,表示該 block 在內存中的大小nFree:整型,表示剩下有幾個 unit 未被分配nFirst:整型,表示下一個可供分配的 unit 的標識號pNext:指針,指向下一個 memory block
Memory Pool
一個內存池總的管理數據結構,換句話說,是一個內存池對象。
pBlock:指針,指向第一個 memory blocknUnitSize:整型,表示每個 unit 的尺寸nInitSize:整型,表示第一個 block 的 unit 個數nGrowSize:整型,表示在第一個 block 之外再繼續增加的每個 block 的 unit 個數
函數接口
作為一個內存池,需要實現以下一些基本的函數接口,或者說可以是對象方法:
memPoolCreate()
創建一個 memory pool,必須的參數為 unit size,可選參數為上文 memory pool 的 nInitSize 和 nGrowSize。
memPoolDestroy()
銷毀整個 memory pool 並交還給操作系統。
memPoolAlloc()
從 memory pool 中分配一個 unit,其尺寸是預先定義的 unit size。
memPoolFree()
釋放一個指定的 unit。
工作過程
現在我們用一個 unit size 為 1024、init size 為 4(每一個 block 有 4 個 units)的 memory pool 為例,解釋一下內存池的工作原理。下文假設整型的寬度為 4 個字節。
創建 memory pool
程序開始,調用並創建一個 memory pool。此時調用的函數為 memPoolCreate(),程序會創建一個數據結構,相應的結構體成員及其取值如下:

memory pool alloc
當調用者第一次請求 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)處原來的值1 - 將
pData的地址值返回。為便於說明,這塊區域我們標記為 CA
操作后各數據結構的狀態如下:

第二次調用 alloc 的情況類似。調用后各數據結構的狀態如下:

memory pool free
我們先看看結果:

- 首先程序會檢查 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 從內存中釋放掉。
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 結構體需要加上鎖
參考資料
- 《C++應用程序性能優化》 - 內存池 章節
- Memory Pool Basic Concepts
此文已由作者授權騰訊雲+社區發布,更多原文請點擊
搜索關注公眾號「雲加社區」,第一時間獲取技術干貨,關注后回復1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!
