圖文並茂解釋內存池原理

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

  1. 速度遠比 malloc/free 快,由於減小了系統調用的次數,特別是頻繁申請/釋放內存塊的狀況
  2. 避免了頻繁申請/釋放內存以後,系統的大量內存碎片
  3. 節省空間

分類

根據分配出去的內存大小,內存池能夠分爲兩類:算法

Fixed-size Allocation

每次分配出去的內存單元(稱爲 unit 或者 cell)的大小爲程序預先定義的值。釋放內存塊時,則只須要簡單地掛回內存池鏈表中便可。又稱爲 「固定尺寸緩衝池」。apache

常規的作法是:將不一樣 unit size 的內存池整合在一塊兒,以知足不一樣內存塊大小的使用需求segmentfault

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 爲 102四、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 中的內存。返回內存的邏輯以下:

  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 的地址值返回。爲便於說明,這塊區域咱們標記爲 $C_A$

操做後各數據結構的狀態以下:

第二次調用 alloc 的狀況相似。調用後各數據結構的狀態以下:

memory pool free

咱們先看看結果:

  1. 首先程序會檢查 $C_A$ 的地址值,很快就會發現,地址 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

其實能夠看到,上面就是一個簡單的鏈表操做。根據上面的過程,若是 $C_B$ 也釋放了的話,那麼 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 的使用效率是很是高的。


變體或改進

  1. 有些簡化的版本中,能夠不使用 pNext 來維護鏈表,也就是隻有一個 block,而且內存的使用有一個明確且受控的上限值。這常常用在沒有 malloc 系統調用的 RTOS 或者是一些對內存很是敏感的嵌入式系統中。
  2. 若是要用於多線程環境中,那麼 memory pool 結構體須要加上鎖

參考資料


本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

本文地址:http://www.javashuo.com/article/p-ssqgpxhs-km.html
原文發佈於:https://cloud.tencent.com/developer/article/1361759,也是本人的專欄。

相關文章
相關標籤/搜索