記憶體池原理大揭祕

騰訊雲加社群發表於2019-03-01

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

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

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

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

相關文章