內存池技術暢想

內容:

本文將介紹幾種常用的內存池技術的實現,這是我最近學習各大開源的內存池技術遺留下來的筆記,其主要內容包括:

 

    • STL內存池以及類STL內存池實現
    • Memcached內存池實現
    • 固定規格內存池實現 
    • Nginx內存池實現 

 

 

一.類STL的內存池實現方式

SGI STL的內存池分爲一級配置器和二級配置器,

一級配置器主要處理分配空間大小大於128Byte的需求,其內部實現就是直接使用malloc  realloc 和free.

二級配置器則使用使用free_list的數組鏈表的方式來管理內存,SGI的Allocate最小的分辨單位爲8Byte,其free_list數組存着8*n(n=1...16)大小內存的首地址,大小同樣的內存塊使用鏈表的形式相連

  free_list[0] --------> 8 byte

  free_list[1] --------> 16 byte

  free_list[2] --------> 24 byte

  free_list[3] --------> 32 byte
  ... ...
  free_list[15] -------> 128 byte

 因爲其對內存的管理的最小分辨度爲8Byte,所以當我們申請的內存空間不是8的倍數的時候,內存池會將其調整爲8的倍數大小,這叫內存對齊。當然這也免不了帶來內存浪費,例如我們只需要一個10Byte的大小,內存池經過內存對齊後,會給我們一個16Byte的大小,而剩餘的6Byte,在這次使用中根本沒有用到。(對於chunk_allocate的優化請見探究操作系統的內存分配(malloc)對齊策略一文的末尾處)
 

類STL的內存池一般都有如下API

void* allocate(size_t __n) //外部API,分配內存
void deallocate(void* __p, size_t __n)//外部API,回收內存,以供再利用   
char*  chunk_alloc(size_t __size, int& __nobjs)//內部函數,用於分配一個大塊

void* refill(size_t n) //內部函數,用於allocate從free_list中未找到可使用的塊時調用

 這種內存池的工作流程大致如下:

  • 外部調用 allocate向內存池申請內存
  • allocate通過內存對齊的方式在free_list找到合適的內存塊鏈表頭
  • 判斷鏈表頭是否爲NULL,爲NULL則表示沒有此規格空閒的內存,如果不爲NULL,則返那塊內存地址,並將此塊內存地址移除它對應的鏈表
  • 如果爲NULL,則調用refill在freelist上掛載20個此規格的內存空間(形成鏈表),也就是保證此規格的內存空間下次請求時夠用 
  • refill的內部調用了chunk_alloc函數,chunk_alloc的職責就是負責內存池的所有內存的生產,在生產的時候他爲了保證下次能有內存用,所以會將空間*2,所以這個申請流程總的內存消耗爲:(對需求規格內存對齊後的大小)*20*2
 
下面舉一個例子來簡單得說明一下:
  •     當第一次調用chunk_alloc(32,10)的時候,表示我要申請10塊__Obje(free_list), 每塊大小32B,此時,內存池大小爲0,從堆空間申請32*20的大小的內存,把其中32*10大小的分給free_list[3]。
  •    我再次申請64*5大小的空間,此時free_list[7]爲0, 它要從內存池提取內存,而此時內存池剩下320B,剛好填充給free_list[7],內存池此時大小爲0。
  •    第三次請求72*10大小的空間,此時free_list[8]爲0,它要從內存池提取內存,此時內存池空間不足,再次從堆空間申請72*20大小的空間,分72*10給free_list用。 

首次申請20Byte後的狀態圖: 

 

 在未設置預分配的STL內存池中,某個中間狀態的整體圖

 

 

由於STL源碼可閱讀性不強,各種宏等等滿目不堪,所以我這裏就不貼SGI 的源碼了,我在這裏貼一個簡單易懂的山寨版本, 基本的思路是一模一樣的,這個實現沒有了一級和二級配置器,而是在需要的時候直接malloc或者從free_list找。

複製代碼
  1 .
  2 #ifndef MEMORYPOOL_H
  3 #define MEMORYPOOL_H
  4 
  5 #include <stdio.h>
  6 #include <assert.h>
  7 
  8 using namespace std;
  9 
 10 class MemoryPool 
 11 {
 12 private:
 13   
 14     // Really we should use static const int x = N
 15     // instead of enum { x = N }, but few compilers accept the former.
 16     enum {__ALIGN = 8};                            //小型區塊的上調邊界,即小型內存塊每次上調8byte
 17     enum {__MAX_BYTES = 128};                    //小型區塊的上界
 18     enum {__NFREELISTS = __MAX_BYTES/__ALIGN};    //free-lists的個數,爲:16,每個free-list管理不同大小內存塊的配置
 19 
 20   //將請求的內存大小上調整爲8byte的倍數,比如8byte, 16byte, 24byte, 32byte
 21   static size_t ROUND_UP(size_t bytes)
 22   {
 23         return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
 24   }
 25 
 26   union obj 
 27   {
 28       union obj* free_list_link;        //下一個區塊的內存地址,如果爲NULL,則表示無可用區塊
 29       char client_data[1];                //內存區塊的起始地址          
 30   };
 31   
 32 private:
 33     static obj *free_list[__NFREELISTS];    // __NFREELISTS = 16
 34     /*
 35         free_list[0] --------> 8 byte(free_list[0]管理8bye區塊的配置)
 36         free_list[1] --------> 16 byte
 37         free_list[2] --------> 24 byte
 38         free_list[3] --------> 32 byte
 39         ... ...
 40         free_list[15] -------> 128 byte
 41     */
 42 
 43   //根據區塊大小,決定使用第n號的free_list。n = [0, 15]開始
 44   static  size_t FREELIST_INDEX(size_t bytes) 
 45   {
 46         return (((bytes) + __ALIGN-1)/__ALIGN - 1);
 47   }
 48 
 49   // Returns an object of size n, and optionally adds to size n free list.
 50   static void *refill(size_t n);
 51   
 52   // 配置一大塊空間,可容納nobjs個大小爲size的區塊
 53   // 如果配置nobjs個區塊有所不便,nobjs可能會降低
 54   static char *chunk_alloc(size_t size, int &nobjs);
 55 
 56   // Chunk allocation state.
 57   static char *start_free;        //內存池起始位置
 58   static char *end_free;        //內存池結束位置
 59   static size_t heap_size;        //內存池的大小
 60 
 61 public:
 62 
 63   // 公開接口,內存分配函數     
 64     static void* allocate(size_t n)
 65     {
 66         obj** my_free_list = NULL;
 67         obj* result = NULL;
 68 
 69         //如果待分配的內存字節數大於128byte,就調用C標準庫函數malloc
 70         if (n > (size_t) __MAX_BYTES) 
 71         {
 72             return malloc(n);
 73         }
 74 
 75         //調整my_free_lisyt,從這裏取用戶請求的區塊
 76         my_free_list = free_list + FREELIST_INDEX(n);
 77     
 78 
 79         result = *my_free_list;        //欲返回給客戶端的區塊
 80 
 81         if (result == 0)    //沒有區塊了
 82         {
 83             void *r = refill(ROUND_UP(n));
 84 
 85             return r;
 86         }
 87     
 88         *my_free_list = result->free_list_link;        //調整鏈表指針,使其指向下一個有效區塊
 89     
 90         return result;
 91     };
 92 
 93 
 94     //歸還區塊
 95     static void deallocate(void *p, size_t n)
 96     {
 97         assert(p != NULL);
 98 
 99         obj* q = (obj *)p;
100         obj** my_free_list = NULL;
101 
102         //大於128byte就調用第一級內存配置器
103         if (n > (size_t) __MAX_BYTES) 
104         {
105             free(p) ;
106         }
107 
108         // 尋找對應的free_list
109         my_free_list = free_list + FREELIST_INDEX(n);
110     
111         // 調整free_lis,回收內存
112         q -> free_list_link = *my_free_list;
113         *my_free_list = q;
114   }
115 
116   static void * reallocate(void *p, size_t old_sz, size_t new_sz);
117 
118 } ;
119 
120 
121 /* We allocate memory in large chunks in order to avoid fragmenting     */
122 /* the malloc heap too much.                                            */
123 /* We assume that size is properly aligned.                             */
124 /* We hold the allocation lock.                                         */
125 
126 // 假設size已經上調至8的倍數
127 // 注意nobjs是passed by reference,是輸入輸出參數
128 char* MemoryPool::chunk_alloc(size_t size, int& nobjs)
129 {
130     char* result = NULL;    
131     
132     size_t total_bytes = size * nobjs;                //請求分配內存塊的總大小
133     size_t bytes_left = end_free - start_free;        //內存池剩餘空間的大小
134 
135     if (bytes_left >= total_bytes)     //內存池剩餘空間滿足要求量
136     {
137         result = start_free;
138         start_free += total_bytes;
139         
140         return result;
141     } 
142     else if (bytes_left >= size)         //內存池剩餘空間不能完全滿足需求量,但足夠供應一個(含)以上的區塊
143     {
144         nobjs = bytes_left/size;        //計算內存池剩餘空間足夠配置的區塊數目
145         total_bytes = size * nobjs;
146         
147         result = start_free;
148         start_free += total_bytes;
149         
150         return result;
151     } 
152     else         //內存池剩餘空間連一個區塊都無法提供
153     {
154         //bytes_to_get爲內存池向malloc請求的內存總量
155         size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
156         
157         // Try to make use of the left-over piece.
158         if (bytes_left > 0
159         {
160             obj** my_free_list = free_list + FREELIST_INDEX(bytes_left);
161 
162             ((obj *)start_free) -> free_list_link = *my_free_list;
163             *my_free_list = (obj *)start_free;
164         }
165 
166         // 調用malloc分配堆空間,用於補充內存池
167         start_free = (char *)malloc(bytes_to_get);
168         if (0 == start_free)     //heap空間已滿,malloc分配失敗
169         {
170             int i;
171             obj ** my_free_list, *p;
172 
173             //遍歷free_list數組,試圖通過釋放區塊達到內存配置需求
174             for (i = size; i <= __MAX_BYTES; i += __ALIGN) 
175             {
176                 my_free_list = free_list + FREELIST_INDEX(i);
177                 p = *my_free_list;
178                 
179                 if (0 != p) 
180                 {
181                     *my_free_list = p -> free_list_link;
182                     start_free = (char *)p;
183                     end_free = start_free + i;
184                     
185                     return chunk_alloc(size, nobjs);
186                     // Any leftover piece will eventually make it to the
187                     // right free list.
188                 }
189             }
190 
191             end_free = 0;    // In case of exception.
192 
193             // 調用第一級內存配置器,看看out-of-memory機制能否盡點力
194             
195             // This should either throw an
196             // exception or remedy the situation.  Thus we assume it
197             // succeeded.
198         }
199         
200         heap_size += bytes_to_get;
201         end_free = start_free + bytes_to_get;
202         
203         return chunk_alloc(size, nobjs);
204     }
205     
206 }
207 
208 
209 /* Returns an object of size n, and optionally adds to size n free list.*/
210 /* We assume that n is properly aligned.                                */
211 /* We hold the allocation lock.                                         */
212 void* MemoryPool::refill(size_t n)
213 {
214     int nobjs = 20;
215 
216     // 注意nobjs是輸入輸出參數,passed by reference。
217     char* chunk = chunk_alloc(n, nobjs);
218     
219     obj* * my_free_list = NULL;
220     obj* result = NULL;
221     obj* current_obj = NULL;
222     obj* next_obj = NULL;
223     int i;
224 
225     // 如果chunk_alloc只獲得了一個區塊,這個區塊就直接返回給調用者,free_list無新結點
226     if (1 == nobjs) 
227     {
228         return chunk;
229     }
230 
231     // 調整free_list,納入新結點
232     my_free_list = free_list + FREELIST_INDEX(n);
233 
234     result = (obj*)chunk;    //這一塊返回給調用者(客戶端)
235 
236 
237     //用chunk_alloc分配而來的大量區塊配置對應大小之free_list  
238     *my_free_list = next_obj = (obj *)(chunk + n);
239       
240     for (i = 1; ; i++) 
241     {
242         current_obj = next_obj;
243         next_obj = (obj *)((char *)next_obj + n);
244         
245         if (nobjs - 1 == i) 
246         {
247             current_obj -> free_list_link = NULL;
248             break;
249         } 
250         else 
251         {
252             current_obj -> free_list_link = next_obj;
253         }
254     }
255       
256     return result;
257 }
258 
259 //重新配置內存,p指向原有的區塊,old_sz爲原有區塊的大小,new_sz爲新區塊的大小
260 void* MemoryPool::reallocate(void *p, size_t old_sz, size_t new_sz)
261 {
262     void* result = NULL;
263     size_t copy_sz = 0;
264 
265     if (old_sz > (size_t) __MAX_BYTES && new_sz > (size_t) __MAX_BYTES) 
266     {
267         return realloc(p, new_sz);
268     }
269 
270     if (ROUND_UP(old_sz) == ROUND_UP(new_sz)) 
271     {
272         return p;
273     }
274 
275     result = allocate(new_sz);
276     copy_sz = new_sz > old_sz? old_sz : new_sz;
277 
278     memcpy(result, p, copy_sz);
279 
280     deallocate(p, old_sz);
281 
282     return result;
283 }
284 
285 //靜態成員變量初始化
286 char* MemoryPool::start_free = 0;
287 
288 char* MemoryPool::end_free = 0;
289 
290 size_t MemoryPool::heap_size = 0;    
291 
292 MemoryPool::obj* MemoryPool::free_list[MemoryPool::__NFREELISTS] 
293                         = {0000000000000000, };
294 
295 #endif
296
複製代碼

 

 

 二.MemCached內存池實現

與類STL內存池不同的是, 用於緩存的內存池不是解決小對象的內存分配可能導致堆內存碎片多的問題,緩存內存池要爲緩存系統的所有存儲對象分配空間,無論大小。因爲緩存系統通常對其佔用的最大內存有限制,所以也就不能在沒有空間用的時候隨便malloc來實現了。 MemCached的內存池的基本想法是避免重複大量的初始化和清理操作。

 
Memcached 中內存分配機制主要理念 
1.  先爲分配相應的大塊內存,再在上面進行無縫小對象填充 
2.  懶惰檢測機制,Memcached 不花過多的時間在檢測各個item對象是否超時,當 get獲取數據時,才檢查item對象是否應該刪除,你不訪問,我就不處理。 
3.  懶惰刪除機制,在 memecached 中刪除一個 item對象的時候,並不是從內存中釋放,而是單單的進行標記處理,再將其指針放入 slot回收插糟,下次分配的時候直接使用。

 

MemCached內存池Slab Allocation的主要術語
Page
分配給Slab的內存空間,默認是1MB。分配給Slab之後根據slab的大小切分成chunk。

 

Chunk
用於緩存記錄的內存空間。
Slab Class

特定大小的chunk的組。 

 

Memcached的內存分配以page爲單位,默認情況下一個page是1M ,可以通過-I參數在啓動時指定。如果需要申請內存 時,memcached會劃分出一個新的page並分配給需要的slab區域。Memcached並不是將所有大小的數據都放在一起的,而是預先將數據空間劃分爲一系列slabs,每個slab只負責一定範圍內的數據存儲,其大小可以通過啓動參數設置增長因子,默認爲1.25,即下一個slab的大小是上一個的1.25倍。如 下圖,每個slab只存儲大於其上一個slab的size並小於或者等於自己最大size的數據。如下圖所示,需要存儲一個100Bytes的對象時,會選用112Bytes的Slab Classes

 

 基於這種實現的內存池也會遇到STL內存池一樣的問題,那就是資源的浪費,我只需要100Byte的空間,你卻給了我128Bytes,剩餘的28Bytes就浪費了

 

 

其主要API:

slabs_init() 
slab初始化,如果配置時採用預分配機制(prealloc)則在先在這使用malloc分配所有內存。 
再根據增長因子factor 給每個 slabclass 分配容量。 
slabs_clsid() 
計算出哪個 slabclass 適合用來儲存大小給定爲 size的item, 如果返回值爲 0則存儲的物件過大,無法進行存儲。 
do_slabs_alloc() 
在這個函數裏面,由宏定義來決定採用系統自帶的 malloc 機制還是 memcached的slab機制對內存進行分配,理所當然,在大多數情況下,系統的malloc會比slab慢上一個數量級。 分配時首先考慮slot 內的空間(被回收的空間),再檢查 end_page_ptr 指針指向的的空閒空間,還是沒有的空間的話,再試試分配新的內存。如果所有空間都用盡的時候,則返回NULL表示目前資源已經枯竭了。 
do_slabs_free() 
首先檢查當目前的插糟是否已經達到可用總插糟的總容量,如果達到就爲其重新分配空間,再將該回收的 item的指針插入對應當前 id的 slabclass 的插糟 (slots) 之中。  

 

 關於MemCached還有個問題需要解釋下,在預分配的場景下,有的同事認爲MemCached不適合大量存儲某個特定大小範圍內的對象,他們認爲預分配的條件下,每個SlabClasses的總大小是固定的(爲一個Page),其實不是,MemCached預分配並不會消耗掉所有的內存,在請求空間的時候,如果發現這個型號的Chunks都被用完了,就會新增一個分頁到這個Slab Classes,所以是不會出現那位同事說的那個問題的...(可見代碼slabs.c中do_slabs_alloc函數中do_slabs_newslab的調用)

 

三.固定大小內存池

上面兩種內存池的實現,都會造成一定程度的內存浪費,如果我存的對象大小基本是固定的,儘管有很多不同的對象,有沒有不會浪費內存的的簡單方式呢?

既然需要存的對象大小是固定的,那麼我們的內存池對於內存的管理可以這樣實現:

class IovecContainer
{
public:
list<char*> m_objList;

}; 

class MemoryPool
{
public:
void* allocate(size_t __n) //外部API,分配內存
void deallocate(void* __p, size_t __n)//外部API,回收內存,以供再利用   
private:
map<int, IovecContainer* > m_mapPool;

char*  chunk_alloc(size_t __size, int& __nobjs)//內部函數,用於分配一個大塊
void* refill(size_t n) //內部函數,用於allocate從free_list中未找到可使用的塊時調用 

}; 

這樣的實現對於這個特定的需求非常好用,不回浪費掉剩餘空間,但是這樣的實現侷限性就高了,我們不能用這個內存池來存儲大小不定的對象(如string),如果用了,此內存池形同虛設,並且還浪費內存,所以具體怎麼選擇還是要看需求來定... 

 

 四.Nginx內存池實現
 關於Nginx內存池實現網上有比較多的分析文章,這裏我就不重複造輪子了,直接貼鏈接,有興趣的可以關注下:

 

http://blog.csdn.net/v_july_v/article/details/7040425
http://bbs.chinaunix.net/thread-3626006-1-1.html;
http://blog.csdn.net/livelylittlefish/article/details/6586946;
http://blog.chinaunix.net/space.php?uid=7201775;

淘寶數據共享平臺博客:http://www.tbdata.org/archives/1390 


發佈了24 篇原創文章 · 獲贊 9 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章