內存池原理大揭祕

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~html

本文由[amc](cloud.tencent.com/developer/u…)發表於雲+社區專欄算法

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

  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 爲 102四、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 送你一份技術課程大禮包!

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

相關文章
相關標籤/搜索