SGI STL 的內存管理

1. 好多廢話

    在分析完nginx的內存池之後,也想了解一下C++的內存管理,於是就很自然得想到STL。

STL是一個重量級的作品,據說當時的出現,完全可以說得上是一個劃時代意義的作品。

泛型、數據結構和算法的分離、底耦合、高複用… 啊,廢話不多說了,再說下去讓人感覺像

王婆賣瓜了。

    啊,還忘了得加上兩位STL大師的名字來聊表我的敬意了。泛型大牛Alexander Stepanov

和 Meng Lee(李夢--讓人浮想的名字啊)。

 

2. SLT 內存的分配

    以一個簡單的例子開始。

#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. 一個標準的配置器

    要寫一個配置器並不是很難,最重要的問題是如何分配和回收內存。下面看下一個標準(也許只能稱爲典型)

的配置器的實現:

#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;

的時候其實幹了兩件事情:(1) 調用::operator new 申請一塊內存(就是malloc了)

                                  (2) 調用了CSld::CSld();

而在SGI中, 其內存分配把這兩步獨立出了兩個函數:allocate 申請內存, construct 調用構造函數。

他們分別在<stl_alloc.h>, <stl_construct.h> 中。

SGI的內存管理比上面所說的更復雜一些, 首先看一些SGI內存管理的幾個主要文件,如下圖所示:

                  SGI Memory

                                <圖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釋放內存。

可也看下如下的代碼:

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 就是一個內存池了。

我們首先來看一下它的代碼:

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 內部,維護着這樣一個結構體:

  union _Obj {
        union _Obj* _M_free_list_link;
        char _M_client_data[1];    /* The client sees this.        */
  };
static _Obj*  _S_free_list[]; //我就是這樣用的

 

其實一個free_list 就是一個鏈表,如下圖所示:

   link

                   <圖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()沖洗填充該鏈表。

相應的代碼如下:

  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;
  };

 

 

下面畫了一張圖來幫助理解:

         GetMemory

                           <圖3. GetMemory>

 

4.3.3 二級配置器的內存釋放:allocate

    有內存的分配,當然得要釋放了,下面就來看看是如何釋放的:

  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函數從內存池中提取內存空間。

首先我們看一下它的源代碼:

/* 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的定義)。

當申請一塊內存時候,如果內存池總內存量充足,直接分配,不然就各有各的處理方法了。

下面舉一個例子來簡單得說明一下:

   1. 當第一次調用chunk_alloc(32,10)的時候,表示我要申請10塊__Obje(free_list), 每塊大小32B,

此時,內存池大小爲0,從堆空間申請32*20的大小的內存,把其中32*10大小的分給free_list[3](參考圖3)。

   2. 我再次申請64*5大小的空間,此時free_list[7]爲0, 它要從內存池提取內存,而此時內存池剩下320B,

剛好填充給free_list[7],內存池此時大小爲0。

   3. 我第三次神奇一耳光72*10大小的空間,此時free_list[8]爲0,它要從內存池提取內存,此時內存池空間

不足,再次從堆空間申請72*20大小的空間,分72*10給free_list用。

    整一個SGI內存分配的大體流程就是這樣了。

5. 小結

    SIG的內存池比nginx中的複雜多了。簡單得分析一下+寫這篇文章花了我整整3個晚上的時間。

啊,我的青春啊。


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