1. 程式人生 > >深度剖析空間配置器(二)一二級配置器

深度剖析空間配置器(二)一二級配置器

空間配置器

主要分三個檔案實現,我們已經介紹過第一個檔案了(物件的構造和析構   http://blog.csdn.net/hj605635529/article/details/70238270),

現在我們來介紹第二個檔案 stl_alloc.h  也就是空間配置器的精華所在  檔案中定義了一、二兩級配置器   其設計思想為:

向 system heap 要求空間;
考慮多執行緒 (multi-threads) 狀態;
考慮記憶體不足時的應變措施;
考慮過多 “小型區塊” 可能造成的記憶體碎片 (fragment) 問題。

記憶體的配置和釋放

在記憶體配置方面,STL分為兩級配置器,當請求的記憶體大於128b的時候呼叫第一級配置器,當請求的記憶體小於等於128b的時候呼叫第二級配置器。先來看看下面這張表,大概就能知道第一級和第二級配置器主要乾了些什麼,其他的一些細節如記憶體池是怎麼工作的,下面會給出具體解釋。

記憶體配置記憶體配置

第一級配置器

//一級配置器
template <int __inst>
class __malloc_alloc_template {

private:

	//呼叫malloc函式不成功後呼叫
  static void* _S_oom_malloc(size_t);
  //呼叫realloc函式不成功後呼叫 
  static void* _S_oom_realloc(void*, size_t);

  //類似於C++的set_new_handle錯誤處理函式一樣,如果不設定,在記憶體不足時,返回THROW_BAD_ALLOC
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif

public:

	//第一級配置器分配記憶體
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);  //直接呼叫malloc來分配記憶體
    if (0 == __result) __result = _S_oom_malloc(__n); //如果分配失敗,則呼叫_S_oom_malloc()函式
    return __result;
  }
	//第一級配置器直接呼叫free釋放記憶體。
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }
//直接呼叫reallloc來分配記憶體
  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);//如果realloc分配不成功,呼叫_S_oom_realloc()
    return __result;
  }

  //異常處理函式,記憶體分配失敗後的處理
  //  函式的返回值是一個函式指標,引數也是一個函式指標,這兩個函式指標的型別都是返回值為void,引數為void的函式
  static void (* __set_malloc_handler(void (*__f)()))() 
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }

};

// malloc_alloc out-of-memory handling

#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG

// 以下是針對記憶體分配失敗後的處理
//首先,將__malloc_alloc_oom_handler的預設值設為0

template <int __inst>
void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;


template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) { // 不斷地嘗試釋放、再配置、再釋放、再配置
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }//這裡是當沒有設定,直接丟擲異常
        (*__my_malloc_handler)();// 呼叫處理例程,嘗試釋放記憶體
        __result = malloc(__n); // 再重新分配記憶體
        if (__result) return(__result);// 如果分配成功則返回指標
    }
}

template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;

    for (;;) { //不斷地嘗試釋放、再配置、再釋放、再配置
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }//這裡是當沒有設定,直接丟擲異常
        (*__my_malloc_handler)();// 呼叫處理例程,嘗試釋放記憶體
        __result = realloc(__p, __n);// 再重新分配記憶體
        if (__result) return(__result);// 如果分配成功則返回指標
    }
}
  //不斷的嘗試釋放和申請是因為使用者不知道還需要釋放多少記憶體來滿足分配需求,只能逐步的釋放申請
上面並非死迴圈,它有兩個退出條件:1.使用者沒有定義相應的記憶體不足處理例程,即沒有通過釋放記憶體來解決現有記憶體分配不足的問題,結果丟擲異常,直接退出(巨集定義);2.在使用者定義了釋放記憶體程式例程後,成功分配指定大小記憶體,返回指向該記憶體區域的首地址。 可以很清楚的看出,第一級配置器以 malloc(),free(),realloc() 等 C 函式執行實際的記憶體申請、釋放、重申請操作, 總結:SGI 第一級配置器的 allocate() 和 reallocate() 都是在呼叫 malloc() 和 realloc() 不成功後,改呼叫 _S_oom_malloc() 和 _S_oom_realloc()。後兩者都有內迴圈,不斷呼叫 “記憶體不足處理例程”,期望在某次呼叫之後,可以獲得足夠的記憶體來完成所需求的記憶體分配,如果 “記憶體不足處理例程” 並未被客端設定,_S_oom_malloc() 和 _S_oom_realloc() 便會呼叫 __THROW_BAD_ALLOC,丟出 bad_alloc 異常資訊,而後直接利用 exit(1) 中止程式。

第二級配置器


我們重點來看看第二級配置器,這才是SGI的經典,我們需要再次知道第二級分配器是怎麼“被”工作的。 (1)當用戶申請的記憶體大於128bytes時,SGI配置器就會將這個工作交由第一級分配器來完成 (2)當用戶申請的記憶體大小小於128bytes時,SGI配置器就會將這個工作交由第二級分配器來完成。此時,第二級分配器就要開始工作了。第二級分配器的原理較為簡單,就是向記憶體池中申請一大塊記憶體空間,然後按照大小分為16組,(8,16…..128),每一個大小都對應於一個free_list連結串列,這個連結串列上面的節點就是可以使用的記憶體空間,需要注意的是,配置器只能分配8的倍數的記憶體,如果使用者申請的記憶體大小不足8的倍數,配置器將自作主張的為使用者上調到8的倍數。

當然,第二級配置器的原理遠沒有這麼簡單,上面我們說到第二級配置器如何管理記憶體,現在,我們要開始為使用者分配記憶體和回收記憶體了。
當用戶申請一個記憶體後,第二級配置器首先將這個空間大小上調到8的倍數,然後找到對應的free_list連結串列, (1)如果連結串列尚有空閒節點,那麼就直接取下一個節點分配給使用者, (2)如果對應連結串列為空,那麼就需要重新申請這個連結串列的節點,預設為20個此大小的節點。 (3)如果記憶體池已經不足以支付20個此大小的節點,但是足以支付一個或者更多的該節點大小的記憶體時,返回可完成的節點個數。 (4)如果已經沒有辦法滿足該大小的一個節點時,就需要重新申請記憶體池了!所申請的記憶體池大小為:2*total_bytes+ROUND_UP(heap_size>>4),total_bytes是所申請的記憶體大小,SGI將申請2倍還要多的記憶體。為了避免記憶體碎片問題,需要將原來記憶體池中剩餘的記憶體分配給free_list連結串列。 (4)的情況分為兩種: 1)如果記憶體池申請記憶體失敗了,也就是heap_size不足以支付要求時,SGI的次級配置器將使用最後的絕招檢視free_list陣列,檢視是否有足夠大的沒有被使用的記憶體, 2)如果這些辦法都沒有辦法滿足要求時,只能呼叫第一級配置器了,我們需要注意,第一級配置器雖然是用malloc來分配記憶體,但是有new-handler機制(out-of-memory),如果無法成功,只能丟擲bad_alloc異常而結束分配。
enum { _ALIGN = 8 };                //小型區塊的上調邊界  
enum { _MAX_BYTES = 128 };          //小型區塊的上限  
enum { _NFREELISTS = 16 }; // _MAX_BYTES/_ALIGN   //free-list 編號數  
  
//配置記憶體後,維護對應記憶體塊的空閒連結串列節點結構  
union _Obj {  
    union _Obj* _M_free_list_link;   //空閒連結串列  
    char _M_client_data[1];    /* The client sees this. 使用者使用的*/  
};  
如此巧妙地運用union來管理節點,如果還沒有被分配,那麼free_list_link有效,如果已經分配給使用者,那麼client_data[1]有效。不會造成多餘的浪費!

enum {__ALIGN = 8};   //小型區塊的上調邊界
enum {__MAX_BYTES = 128};  //小型區塊的上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN};   //free-lists個數
//第一引數用於多執行緒,這裡不做討論。
template <bool threads, int inst>  
class __default_alloc_template  
{  
private:
	// 此函式將bytes的邊界上調至8的倍數
	static size_t ROUND_UP(size_t bytes)  
	{  
	return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));  
	}  
private:    
	// 此union結構體上面已經解釋過了
	union obj  
	{  
	union obj * free_list_link;  
	char client_data[1];
	};  
private: 
	//16個free-lists
	static obj * __VOLATILE free_list[__NFREELISTS];
	// 根據待待分配的空間大小, 在free_list中選擇合適的大小  
	static  size_t FREELIST_INDEX(size_t bytes)  
	{  
	return (((bytes) + __ALIGN-1)/__ALIGN - 1);  
	}
	// 返回一個大小為n的物件,並可能加入大小為n的其它區塊到free-lists
	static void *refill(size_t n);  
	// 配置一大塊空間,可容納nobjs個大小為“size”的區塊
	// 如果配置nobjs個區塊有所不便,nobjs可能會降低,所以需要用引用傳遞
	static char *chunk_alloc(size_t size, int &nobjs);  
	// 記憶體池  
	static char *start_free;      // 記憶體池起始點,只在chunk_alloc()中變化 
	static char *end_free;        // 記憶體池結束點,只在chunk_alloc()中變化 
	static size_t heap_size;      // 已經在堆上分配的空間大小
public:
	static void* allocate(size_t n);// 空間配置函式
	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>::start_free = 0;  
// 記憶體池結束位置  
template <bool threads, int inst>  
char *__default_alloc_template<threads, inst>::end_free = 0;  
// 已經在堆上分配的空間大小
template <bool threads, int inst>  
size_t __default_alloc_template<threads, inst>::heap_size = 0;  
// 記憶體池容量索引陣列  
template <bool threads, int inst>  
__default_alloc_template<threads, inst>::obj * __VOLATILE  
__default_alloc_template<threads, inst> ::free_list[__NFREELISTS ] = 
	{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };

看完上面這一堆原始碼,你可能早就頭暈眼花,一臉懵逼了,沒事,我再來用一張思維導圖來幫你理一理思緒:

二級配置器1二級配置器1

接下來又是枯燥的原始碼時間!相信有上面這張圖,看原始碼的思路就比較清晰了。

空間配置函式allocate()

借用《STL原始碼剖析》裡面的一張圖,來說明空間配置函式的呼叫過程:(看圖放鬆,放鬆完繼續看原始碼!別偷懶)

空間配置函式空間配置函式


static void * allocate(size_t n)  
{  
	obj * volatile * my_free_list;  
	obj * result;  
	// 大於128就呼叫第一級配置器
	if (n > (size_t) __MAX_BYTES) {  
	 return(malloc_alloc::allocate(n));  
	}  
	// 尋找16個free_lists中適當的一個
	my_free_list = free_list + FREELIST_INDEX(n);  
	result = *my_free_list;  
	if (result == 0) {  
		// 如果沒有可用的free list,準備重新填充free_list
		void *r = refill(ROUND_UP(n));  
		return r;  
	}
	// 調整free list
	*my_free_list = result -> free_list_link;  
	return (result);  
};

重新填充函式refill()

template <bool threads, int inst>  
void* __default_alloc_template<threads, inst>::refill(size_t n)  
{  
	int nobjs = 20;  //	預設獲取20個
	char * chunk = chunk_alloc(n, nobjs);  //找記憶體池要空間
	obj * volatile * my_free_list;  
	obj * result;  
	obj * current_obj, * next_obj;  
	int i;  
	// 如果記憶體池僅僅只夠分配一個物件的空間, 直接返回即可  
	if(1 == nobjs) return(chunk);  
	// 記憶體池能分配更多的空間,調整free_list納入新節點
	my_free_list = free_list + FREELIST_INDEX(n);
	// 在chunk的空間中建立free_list  
	result = (obj *)chunk;
	*my_free_list = next_obj = (obj *)(chunk + n); //導引free_list指向新配置的空間(取自記憶體池)
	for(i = 1; ; i++) {	//從1開始,因為第0個返回給客端
		current_obj = next_obj;  
		next_obj = (obj *)((char *)next_obj + n);  
		if(nobjs - 1 == i) {  
			current_obj -> free_list_link = 0;  
			break;  
		} 
		else {  
			current_obj -> free_list_link = next_obj;  
		}  
	}  
	return(result);//返回頭指標
}

記憶體池函式chunk_alloc()

template <bool threads, int inst>  
char*  
__default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs)  
{  
    char * result;  
    size_t total_bytes = size * nobjs;  
    size_t bytes_left = end_free - start_free;  // 計算記憶體池剩餘容量  
   
    //記憶體池中的剩餘空間滿足需求 
    if (bytes_left >= total_bytes) {  
        result = start_free;
        start_free += total_bytes;
        return(result);//返回起始地址
    }  
    // 如果記憶體池中剩餘的容量不夠分配, 但是能至少分配一個節點時,  
    // 返回所能分配的最多的節點, 返回start_free指向的記憶體塊  
    // 並且重新設定記憶體池起始點  
    else if(bytes_left >= size) {
        nobjs = bytes_left/size;  
        total_bytes = size * nobjs;  
        result = start_free;  
        start_free += total_bytes;  
        return(result);  
    }  
    // 記憶體池剩餘記憶體連一個節點也不夠分配  
    else {  
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);  
        // 將剩餘的記憶體分配給指定的free_list[FREELIST_INDEX(bytes_left)]  
        if (bytes_left > 0) {  
        	//記憶體池內還有一些零頭,先分給適當的free_list
        	//尋找適當的free_list
            obj * __VOLATILE * my_free_list =  
                   	free_list + FREELIST_INDEX(bytes_left);
            // 調整free_list,將記憶體池中的殘餘空間編入 
            ((obj *)start_free) -> free_list_link = *my_free_list;  
            *my_free_list = (obj *)start_free;  
        }  
        start_free = (char *)malloc(bytes_to_get);  
        // 分配失敗, 搜尋原來已經分配的記憶體塊, 看是否有大於等於當前請求的記憶體塊  
        if (0 == start_free) {// heap裡面空間不足,malloc失敗
            int i;  
            obj * __VOLATILE * my_free_list, *p;  
            // 試著檢查檢查free_list中的可用空間,即尚有未用的空間,且區塊夠大  
            for (i = size; i <= __MAX_BYTES; i += __ALIGN) {  
                my_free_list = free_list + FREELIST_INDEX(i);  
                p = *my_free_list;  
                // 找到了一個, 將其加入記憶體池中  
                if (0 != p) {  
                    *my_free_list = p -> free_list_link;  
                    start_free = (char *)p;  
                    end_free = start_free + i;  
                    // 記憶體池更新完畢, 重新分配需要的記憶體  
                    return(chunk_alloc(size, nobjs));  
                    //任何剩餘零頭將被編入適當的free_list以留備用 
               }  
            }  
 
        // 再次失敗, 直接呼叫一級配置器分配, 期待異常處理函式能提供幫助  
        // 不過在我看來, 記憶體分配失敗進行其它嘗試已經沒什麼意義了,  
        // 最好直接log, 然後讓程式崩潰  
        end_free = 0;
        	//呼叫第一級配置器,看看out-of-memory機制能不能起點作用
            start_free = (char *)malloc_alloc::allocate(bytes_to_get);  
        }
        heap_size += bytes_to_get;  
        end_free = start_free + bytes_to_get;  
        // 記憶體池更新完畢, 重新分配需要的記憶體  
        return(chunk_alloc(size, nobjs));  
    }  
}

記憶體釋放函式deallocate()

記憶體釋放函式會將釋放的空間交還給free_list以留備用。其過程如下圖所示:

空間釋放函式空間釋放函式

其實就是一個簡單的單鏈表插入的過程。其原始碼如下:

static void deallocate(void *p, size_t n)  
{  
	obj *q = (obj *)p;  
	obj * volatile * my_free_list;  
	// 大於128的直接交由第一級配置器釋放  
	if (n > (size_t) __MAX_BYTES) {  
		malloc_alloc::deallocate(p, n);  
		return;  
	}
	// 尋找適當的free_list  
	my_free_list = free_list + FREELIST_INDEX(n);  
	// 調整free_list,回收區塊
	q -> free_list_link = *my_free_list;  
	*my_free_list = q;  
}

配置器的使用

通過以上的圖和原始碼,基本上將STL的兩層配置器講完了,接下來就來熟悉一下怎麼使用配置器。

STL將上述配置器封裝在類simple_alloc中,提供了四個用於記憶體操作的介面函式,分別如下:

template<class T, class Alloc>
class simple_alloc {
public:
    static T *allocate(size_t n)
                { return 0 == n? 0 : (T*) Alloc::allocate(n * sizeof (T)); }
    static T *allocate(void)	
                { return (T*) Alloc::allocate(sizeof (T)); }
    static void deallocate(T *p, size_t n) 
                { if (0 != n) Alloc::deallocate(p, n * sizeof (T)); }
    static void deallocate(T *p)
                { Alloc::deallocate(p, sizeof (T)); }
};