1. 前言
在分析完 nginx 的記憶體池之後,也想了解一下 C++ 的記憶體管理,於是就很自然得想到 STL。STL 是一個重量級的作品,據說當時的出現,完全可以說得上是一個劃時代意義的作品。泛型、資料結構和演算法的分離、底耦合、高複用… 啊,廢話不多說了,再說下去讓人感覺像王婆賣瓜了。
啊,還忘了得加上兩位 STL 大師的名字來聊表我的敬意了。泛型大牛 Alexander Stepanov 和 Meng Lee(李夢 — 讓人浮想的名字啊)。
2. SLT 記憶體的分配
以一個簡單的例子開始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <vector> #include <algorithm> using namespace std; void print( int elem) { cout << elem << ' '; } int main() { vector<int> vec; for (int i = 0; i != 10; ++i) vec.push_back(i); for_each(vec.begin(), vec.end(), print); //請允許我賣弄一點點小特性 cout << endl; return 0; } |
我們想知道的時候, 當 vec 宣告的時候和 push_back 的時候,是怎麼分配的。
其實對於一個標準的 STL 容器,當 Vetor<int> vec 的真實語句應該是 vetor<int, allocator<int>>vec,allocator 是一個標準的配置器,其作用就是為各個容器管理記憶體。這裡需要注意的是在 SGI STL 中,有兩個配置器:allocator(標準的) 和 alloc(自己實現的,非常經典,這篇文章的主要目的就是為了分析它)。
3. 一個標準的配置器
要寫一個配置器並不是很難,最重要的問題是如何分配和回收記憶體。下面看下一個標準(也許只能稱為典型)的配置器的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
#include <new>// for new #include <cstddef> // size_t #include <climits> // for unit_max #include <iostream> // for cerr using namespace std; namespace SLD { template <class T> class allocator { public: typedef T value_type; typedef T* pointer; typedef const T* const_pointer; typedef T& reference; typedef const T& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; template <class U> struct rebind { typedef allocator<U> other; }; //申請記憶體 pointer allocate(size_type n, const void* hint = 0) { T* tmp = (T*)(::operator new((size_t)(n * sizeof(T)))); //operator new 和new operator是不同的 if (!tmp) cerr << "out of memory"<<endl; return tmp; } //釋放記憶體 void deallocate(pointer p) { ::operator delete(p); } //構造 void construct(pointer p, const T& value) { new(p) T1(value); } //析構 void destroy(pointer p) { p->~T(); } //取地址 pointer address(reference x) { return (pointer)&x; } const_pointer const_address(const_reference x) { return (const_pointer)&x; } size_type max_size() const { return size_type(UINT_MAX/sizeof(T)); } }; } |
注:程式碼有比較大的改動,因為主要是為了理解。
在使用的時候, 只需這樣 vector<int, SLD::allocator<int>>vec; 即可。vetor 便會自動呼叫我們的配置器分配記憶體了。要自己寫個配置器完全可以以這個類為模板。 而需要做的工作便是寫下自己的 allocate 和 deallocate 即可。
其實 SGI 的 allocator 就是這樣直接呼叫 operator new 和::operator delete 實現的,不過這樣做的話效率就很差了。
4. SGI STL 中的 alloc
4.1 SGI 中的記憶體管理
SGI STL 預設的介面卡是 alloc,所以我們在宣告一個 vector 的時候實際上是這樣的 vetor<int, alloc<int>>vec. 這個配置器寫得非常經典,下面就來慢慢分析它。
在我們敲下如下程式碼:
CSld* sld = new CSld;的時候其實幹了兩件事情:
- 呼叫::operator new 申請一塊記憶體(就是 malloc 了)
- 呼叫了 CSld::CSld();
而在 SGI 中, 其記憶體分配把這兩步獨立出了兩個函式:allocate 申請記憶體, construct 呼叫建構函式。他們分別在 <stl_alloc.h>, <stl_construct.h> 中。
SGI 的記憶體管理比上面所說的更復雜一些, 首先看一些 SGI 記憶體管理的幾個主要檔案,如下圖所示:
<圖 1. SGI 記憶體管理>
在 stl_construct.h 中定義了兩個全域性函式 construct() 和 destroy() 來管理構造和析構。在 stl_allo.h 中定義了 5 個配置器, 我們現在關心的是 malloc_alloc_template(一級)和 default_alloc_template(二級)。
在 SGI 中,如果用了一級配置器,便是直接使用了malloc() 和 free() 函式,而如果使用了二級介面卡,則如果所申請的記憶體區域大於 128b,直接使用一級介面卡,否則,使用二級介面卡。而 stl_uninitialized.h 中,則定義了一下全域性函式來進行大塊記憶體的申請和複製。
是不是和 nginx 中的記憶體池很相似啊,不過複雜多了。
4.2 一級配置器:__malloc_alloc_template
上面說過, SGI STL 中, 如果申請的記憶體區域大於 128B 的時候,就會呼叫一級介面卡,而一級介面卡的呼叫也是非常簡單的, 直接用 malloc 申請記憶體,用 free 釋放記憶體。
可也看下如下的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class __malloc_alloc_template { private: // oom = out of memroy,當記憶體不足的時候,我要用下面這兩個函式 static void* _S_oom_malloc(size_t); static void* _S_oom_realloc(void*, size_t); public: //申請記憶體 static void* allocate(size_t __n) { void* __result = malloc(__n); //如果不足,我有不足的處理方法 if (0 == __result) __result = _S_oom_malloc(__n); return __result; } //直接釋放掉了 static void deallocate(void* __p, size_t /* __n */) { free(__p); } //重新分配記憶體 static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz) { void* __result = realloc(__p, __new_sz); if (0 == __result) __result = _S_oom_realloc(__p, __new_sz); return __result; } //模擬C++的 set_new_handler,函式, //為什麼要模擬,因為現在用的是C的記憶體管理函式。 static void (* __set_malloc_handler(void (*__f)()))() { void (* __old)() = __malloc_alloc_oom_handler; __malloc_alloc_oom_handler = __f; return(__old); } }; |
好了, 很簡單把,只是對 malloc,free, realloc 簡單的封裝。
4.3 二級配置器:__default_alloc_template
按上文所說的,SGI 的 __default_alloc_template 就是一個記憶體池了。
我們首先來看一下它的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
template <bool threads, int inst> class __default_alloc_template { private: // Really we should use static const int x = N // instead of enum { x = N }, but few compilers accept the former. enum {_ALIGN = 8};//小塊區域的上界 enum {_MAX_BYTES = 128};//小塊區域的下降 enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN,有多少個區域 /*SGI 為了方便記憶體管理, 把128B 分成16*8 的塊*/ //將Byte調到8的倍數 static size_t _S_round_up(size_t __bytes) { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); } //管理記憶體的連結串列,待會會詳細分析這個 union _Obj { union _Obj* _M_free_list_link; char _M_client_data[1]; /* The client sees this. */ }; private: //宣告瞭16個 free_list, 注意 _S_free_list是成員變數 static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS]; //同了第幾個free_list, 即_S_free_list[n],當然這裡是更具區域大小來計算的 static size_t _S_freelist_index(size_t __bytes) { return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1); } // Returns an object of size __n, and optionally adds to size __n free list. static void* _S_refill(size_t __n); // Allocates a chunk for nobjs of size size. nobjs may be reduced // if it is inconvenient to allocate the requested number. static char* _S_chunk_alloc(size_t __size, int& __nobjs); // Chunk allocation state. static char* _S_start_free;//記憶體池的起始位置 static char* _S_end_free;//記憶體池的結束位置 static size_t _S_heap_size;//堆的大小 /*這裡刪除一堆多執行緒的程式碼*/ public: //分配記憶體,容後分析 /* __n must be > 0 */ static void* allocate(size_t __n); //釋放記憶體,容後分析 /* __p may not be 0 */ static void deallocate(void* __p, size_t __n); //從新分配記憶體 static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz); } //下面是一些 成員函式的初始值的設定 template <bool __threads, int __inst> char* __default_alloc_template<__threads, __inst>::_S_start_free = 0; template <bool __threads, int __inst> char* __default_alloc_template<__threads, __inst>::_S_end_free = 0; template <bool __threads, int __inst> size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0; template <bool __threads, int __inst> typename __default_alloc_template<__threads, __inst>::_Obj* __STL_VOLATILE __default_alloc_template<__threads, __inst> ::_S_free_list[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; |
我們最關心的有三點:1. 記憶體池的建立。2. 記憶體的分配。 3. 記憶體的釋放。
4.3.1 SGI 記憶體池的結構
在分析記憶體池的建立之前我們首先需要看下 SGI 記憶體池的結構。
在__default_alloc_template 內部,維護著這樣一個結構體:
1 2 3 4 5 |
union _Obj { union _Obj* _M_free_list_link; char _M_client_data[1]; /* The client sees this. */ }; static _Obj* _S_free_list[]; //我就是這樣用的 |
其實一個 free_list 就是一個連結串列,如下圖所示:
<圖 2. free_list 的連結串列表示>
這裡需要注意的有兩點:
一:SGI 內部其實維護著 16 個 free-list,對應管理的大小為 8,16,32……128.
二:_Obj 是一個 union 而不是 sturct, 我們知道,union 中的所有成員的引用在記憶體中的位置都是相同的。這裡我們用 union 就可以把每一個節點需要的額外的指標的負擔消除掉。
4.3.2 二級配置器的記憶體分配:allocate
比如現在我要申請一塊 30B 的空間,我要怎麼申請呢?
首先會呼叫二級配置器, 呼叫 allocate,在 allocate 函式之內, 從對應的 32B 的連結串列中拿出空間。
如果對應的連結串列空間不足,就會先用填充至 32B,然後用 refill() 沖洗填充該連結串列。
相應的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static void* allocate(size_t __n) { void* __ret = 0; if (__n > (size_t) _MAX_BYTES) { //如果大於128B, 直接呼叫一級配置器 __ret = malloc_alloc::allocate(__n); } else { //找出 16個free-list 中的一個 _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); _Obj* __RESTRICT __result = *__my_free_list; if (__result == 0) //如果滿了,則我refill整一個連結串列 __ret = _S_refill(_S_round_up(__n)); else { *__my_free_list = __result -> _M_free_list_link; __ret = __result; } } return __ret; }; |
下面畫了一張圖來幫助理解:
<圖 3. GetMemory>
4.3.3 二級配置器的記憶體釋放:allocate
有記憶體的分配,當然得要釋放了,下面就來看看是如何釋放的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void deallocate(void* __p, size_t __n) { if (__n > (size_t) _MAX_BYTES) //如果大於128,直接釋放 malloc_alloc::deallocate(__p, __n); else { //找到對應的連結串列 _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__n); _Obj* __q = (_Obj*)__p; //回收,該連結串列 __q -> _M_free_list_link = *__my_free_list; *__my_free_list = __q; // lock is released here } } |
4.3.4 二級配置器的記憶體池:chunk_alloc
前面說過,在分配記憶體時候如果空間不足會呼叫_S_refill 函式,重新填充空間(ps: 如果這是第一個的話,就是建立了)。而_S_refill 最終呼叫的又是 chunk_alloc 函式從記憶體池中提取記憶體空間。
首先我們看一下它的原始碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
/* We allocate memory in large chunks in order to avoid fragmenting */ /* the malloc heap too much. */ /* We assume that size is properly aligned. */ /* We hold the allocation lock. */ template <bool __threads, int __inst> char* __default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size, int& __nobjs) { char* __result; size_t __total_bytes = __size * __nobjs;//申請的總記憶體空間 size_t __bytes_left = _S_end_free - _S_start_free;//記憶體池剩餘的記憶體空間 if (__bytes_left >= __total_bytes) { //如果你能滿足我 __result = _S_start_free; _S_start_free += __total_bytes; 00ff">return(__result); } else if (__bytes_left >= __size) { //如果能滿足我一塊或一塊以上,參考__Obj這個聯合體(free_list) __nobjs = (int)(__bytes_left/__size); __total_bytes = __size * __nobjs; __result = _S_start_free; _S_start_free += __total_bytes; return(__result); } else { //如果連一塊都給不出 size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4); // Try to make use of the left-over piece. if (__bytes_left > 0) { _Obj* __STL_VOLATILE* __my_free_list = _S_free_list + _S_freelist_index(__bytes_left); ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list; *__my_free_list = (_Obj*)_S_start_free; } .//從堆空間重新分配記憶體 _S_start_free = (char*)malloc(__bytes_to_get); if (0 == _S_start_free) { //連堆都沒有記憶體了 size_t __i; _Obj* __STL_VOLATILE* __my_free_list; _Obj* __p; // Try to make do with what we have. That can't // hurt. We do not try smaller requests, since that tends // to result in disaster on multi-process machines. for (__i = __size; __i <= (size_t) _MAX_BYTES; __i += (size_t) _ALIGN) { __my_free_list = _S_free_list + _S_freelist_index(__i); __p = *__my_free_list; if (0 != __p) { *__my_free_list = __p -> _M_free_list_link; _S_start_free = (char*)__p; _S_end_free = _S_start_free + __i; return(_S_chunk_alloc(__size, __nobjs)); // Any leftover piece will eventually make it to the // right free list. } } _S_end_free = 0; // In case of exception. //呼叫一級配置器,主要是為了呼叫_S_oom_malloc壓榨出記憶體來 _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get); // This should either throw an // exception or remedy the situation. Thus we assume it // succeeded. } //更改一下記憶體池 _S_heap_size += __bytes_to_get; _S_end_free = _S_start_free + __bytes_to_get; return(_S_chunk_alloc(__size, __nobjs)); } } |
區間 [_S_start_free, _S_end_free) 便是記憶體池的總空間(參考類:__default_alloc_template 的定義)。
當申請一塊記憶體時候,如果記憶體池總記憶體量充足,直接分配,不然就各有各的處理方法了。
下面舉一個例子來簡單得說明一下:
- 當第一次呼叫 chunk_alloc(32,10) 的時候,表示我要申請 10 塊__Obje(free_list), 每塊大小 32B,此時,記憶體池大小為 0,從堆空間申請 32*20 的大小的記憶體,把其中 32*10 大小的分給 free_list[3](參考圖 3)。
- 我再次申請 64*5 大小的空間,此時 free_list[7] 為 0, 它要從記憶體池提取記憶體,而此時記憶體池剩下 320B,剛好填充給 free_list[7],記憶體池此時大小為 0。
- 我第三次神奇一耳光 72*10 大小的空間,此時 free_list[8] 為 0,它要從記憶體池提取記憶體,此時記憶體池空間不足,再次從堆空間申請 72*20 大小的空間,分 72*10 給 free_list 用。
整一個 SGI 記憶體分配的大體流程就是這樣了。
5. 小結
SIG 的記憶體池比 nginx 中的複雜多了。簡單得分析一下 + 寫這篇文章花了我整整 3 個晚上的時間。
啊,我的青春啊。