內容:
本文將介紹幾種常用的內存池技術的實現,這是我最近學習各大開源的內存池技術遺留下來的筆記,其主要內容包括:
- 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* 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找。
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 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
294
295 #endif
296
二.MemCached內存池實現
與類STL內存池不同的是, 用於緩存的內存池不是解決小對象的內存分配可能導致堆內存碎片多的問題,緩存內存池要爲緩存系統的所有存儲對象分配空間,無論大小。因爲緩存系統通常對其佔用的最大內存有限制,所以也就不能在沒有空間用的時候隨便malloc來實現了。 MemCached的內存池的基本想法是避免重複大量的初始化和清理操作。
特定大小的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:
關於MemCached還有個問題需要解釋下,在預分配的場景下,有的同事認爲MemCached不適合大量存儲某個特定大小範圍內的對象,他們認爲預分配的條件下,每個SlabClasses的總大小是固定的(爲一個Page),其實不是,MemCached預分配並不會消耗掉所有的內存,在請求空間的時候,如果發現這個型號的Chunks都被用完了,就會新增一個分頁到這個Slab Classes,所以是不會出現那位同事說的那個問題的...(可見代碼slabs.c中do_slabs_alloc函數中do_slabs_newslab的調用)
三.固定大小內存池
上面兩種內存池的實現,都會造成一定程度的內存浪費,如果我存的對象大小基本是固定的,儘管有很多不同的對象,有沒有不會浪費內存的的簡單方式呢?
既然需要存的對象大小是固定的,那麼我們的內存池對於內存的管理可以這樣實現:
};
char* chunk_alloc(size_t __size, int& __nobjs)//內部函數,用於分配一個大塊
void* refill(size_t n) //內部函數,用於allocate從free_list中未找到可使用的塊時調用
};
這樣的實現對於這個特定的需求非常好用,不回浪費掉剩餘空間,但是這樣的實現侷限性就高了,我們不能用這個內存池來存儲大小不定的對象(如string),如果用了,此內存池形同虛設,並且還浪費內存,所以具體怎麼選擇還是要看需求來定...
http://blog.csdn.net/v_july_v/article/details/7040425http://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