內存池原理大揭秘


歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐干貨哦~

本文由[amc](https://cloud.tencent.com/developer/user/1024461?fromSource=waitui)發表於雲+社區專欄

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

  1. 速度遠比 malloc/free 快,因為減少了系統調用的次數,特別是頻繁申請/釋放內存塊的情況
  2. 避免了頻繁申請/釋放內存之后,系統的大量內存碎片
  3. 節省空間

分類

根據分配出去的內存大小,內存池可以分為兩類:

Fixed-size Allocation

每次分配出去的內存單元(稱為 unit 或者 cell)的大小為程序預先定義的值。釋放內存塊時,則只需要簡單地掛回內存池鏈表中即可。又稱為 “固定尺寸緩沖池”。

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

Variable-size allocation

不分配固定長度,內存的分配只是在一大塊空閑的內存上滑動。優點是分配效率很高,缺點是成批地回收內存,因為釋放的內存無法直接重復利用。

使用這種需要合理規划每塊內存的管理區域,所以又叫做 “基於區域的” 內存管理。使用這種做法的分配器,舉例有 Apache Portable Runtime 中的 apr_pool 工具。本文不討論這種內存池。


原理和結構

概念和數據結構

定長內存池有一些基本和必要的概念,需要定義在內存池的結構數據中。以下命名方式使用變體的匈牙利命名法,比如 nNextn表示變量類型為整形。類似地,p表示指針。

Memory Unit

每次程序調用 MemPool_Alloc 獲取一個內存區域后,會獲得一塊連續的內存區域。管理一個這樣的內存區域的單元就成為內存單元 unit,有時也稱作 chunk。每個 unit 需要包含以下數據:

  1. nNext:整型數據,表示下一個可供分配的 unit 的標識號。功能請參見后問
  2. pData[]:實際的內存區域,其大小在創建時由調用方指定

Memory Block

一個內存塊,內存塊中保存着一系列的內存單元。

這個數據結構需要包含以下基本信息:

  1. nSize:整型數據,表示該 block 在內存中的大小
  2. nFree:整型,表示剩下有幾個 unit 未被分配
  3. nFirst:整型,表示下一個可供分配的 unit 的標識號
  4. pNext:指針,指向下一個 memory block

Memory Pool

一個內存池總的管理數據結構,換句話說,是一個內存池對象。

  1. pBlock:指針,指向第一個 memory block
  2. nUnitSize:整型,表示每個 unit 的尺寸
  3. nInitSize:整型,表示第一個 block 的 unit 個數
  4. nGrowSize:整型,表示在第一個 block 之外再繼續增加的每個 block 的 unit 個數

函數接口

作為一個內存池,需要實現以下一些基本的函數接口,或者說可以是對象方法:

memPoolCreate()

創建一個 memory pool,必須的參數為 unit size,可選參數為上文 memory pool 的 nInitSizenGrowSize

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(),程序會創建一個數據結構,相應的結構體成員及其取值如下:

img

memory pool alloc

當調用者第一次請求 memPoolAlloc() 時,內存池發現 block 鏈表為空,於是想系統申請內存,創建 memory block,並初始化如下(其中地址值為假設值):

img

其中 nSize = 4112 = sizeof(memPool) + nInitSize * sizeof(memUnit)。每一個 nNext 依次加一,各指代着跟着自己的下一個 unit。最后一個 unit 的 nNext 值無意義,因此不說明其取值。

然后返回需要的 unit 中的內存。返回內存的邏輯如下:

  1. 內存池在 block 中查詢 nFree 成員
  2. 由於 nFree > 0,表示有未分配的 unit,因此繼續在該 block 中查看 nFirst 成員
  3. nFirst 等於 0,表示該 block 中位置為 0 的 unit 可用。因此內存池可以將這個 unit 中的 pData 地址返回給調用方。 pData 的地址值計算方式為:pBlock + sizeof(memBlock) + nFirst * (sizeof(memUnit)) + sizeof(nNext) = 0x10010
  4. nFree 減一
  5. 修改 nFirst 的值,標記下一個可用的 unit。注意這里的 nFirst 切切不能簡單地加一,而是取返回給調用方的 unit 所對應的 nNext 的值,也就是下圖(2)處原來的值 1
  6. pData 的地址值返回。為便於說明,這塊區域我們標記為 CA

操作后各數據結構的狀態如下:

img

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

img

memory pool free

我們先看看結果:

img

  1. 首先程序會檢查 CA 的地址值,很快就會發現,地址 0x10010 位於上述第一個 block 的范圍之內(0x10000 <= 0x10010 <= (0x10000 + 4112))。再計算偏移值可以很快得出其對應的 nNext 標號,也就是上圖中的(2)位置。
  2. 回收 unit,此時需要標記相應的成員值以標示 unit 的回收狀態。首先查看 nFirst 的值,參見上前幅圖,nFirst 的值為 3,表示位置(3)處的 unit 是可用的。因此我們首先把 (2) 處的 nNext 值設置為 3,將其加回到可用 unit 的鏈表中
  3. nFirst 的值修改為 0,也就是代表剛剛回收回來的 unit 的標號,而(2)處的值賦值為 2,表示b(3)的 unit

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

img

到這個時候,由於整個 block 已經完全回收了(nFree == nInitSize),那么根據不同的策略,可以考慮將整個 block 從內存中釋放掉。

block 滿

我們回到 alloc 的邏輯中,可以看到內存池最開始會檢查 block 的 nFree 成員。如果 nFree == 0 的時候,那么就會在該 block 的 pNext 中去找到下一個 block,再去檢查 nFree。如果發現 block 鏈表已經結束了,那就意味着當前所有的 block 已滿,必須創建新的 block。

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


變體或改進

  1. 有些簡化的版本中,可以不使用 pNext 來維護鏈表,也就是只有一個 block,並且內存的使用有一個明確且受控的上限值。這經常用在沒有 malloc 系統調用的 RTOS 或者是一些對內存非常敏感的嵌入式系統中。
  2. 如果要用於多線程環境中,那么 memory pool 結構體需要加上鎖

參考資料

相關閱讀
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由作者授權騰訊雲+社區發布,更多原文請點擊

搜索關注公眾號「雲加社區」,第一時間獲取技術干貨,關注后回復1024 送你一份技術課程大禮包!

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM