STL——空間配置器剖析(一級空間配置器、二級空間配置器的本質及運用場合,是如何用內存池去管理的)

一級空間配置器、二級空間配置器的本質及運用場合,是如何用內存池去管理的

研究了好久才寫好的,主要是二級配置器,大標題小標題什麼的可能沒有安排好,先
寫了原理上的內容,再剖析了各個函數源碼,各個目錄可以看csdn自帶的目錄(👉


如何使用空間配置器

每個容器都會通過默認參數指定好allocator,不需要顯示聲明,如定義一個vector類容器:std::vector<int> vecTemp;而完整的vecTemp聲明應該是vector<int,allocator<int>> vecTemp;

加入自定義了將內存分配指向磁盤或者其他存儲介質空間的allocator,那麼只要在聲明時傳入設計好的allocator,不再使用默認的allocator就行了

那麼,如何設計一個allocator呢?

SGI STL空間配置器架構

一個空間配置器最基本的功能有四:申請內存、構造對象、析構對象、釋放內存;
但是我們想要設計一個在STL中用的空間配置器的話,光有上述四個是不行的,因爲STL對allocator的組成已經規定好了,即有STL規範

由於一個內存配置與釋放通常分兩個階段,而STL allocator 將這兩個階段的操作區分開來:

  1. 內存配置由alloc::allocate()負責,內存釋放由alloc::deallocate()負責
  2. 對象構造由::construct()負責,對象析構由::destroy()負責
    其實,對於內存配置和釋放還有allocator::allocate()allocator::deallocate(), 但它就是對::operator new::operator delete做了一層薄薄的封裝, 效率不好就不要使用了

STL標準規格告訴我們,配置器定義於<memory>中, SGI 包含以下兩個文件:
1, #include<stl_alloc.h>//負責內存空間的配置與釋放
2, #include<stl_construct.h>//負責對象內容的構造與析構

在這裏插入圖片描述

構造和析構的基本工具:construct()和destroy()

書P52~53, 理解是簡單的
在這裏插入圖片描述construct()就是接受一個指針p和一個初值value,用途就是將初值設定到指針所致的空間上,可以通過placement new來完成。

destroy()有兩個版本:第一個版本:接受一個指針,直接調用析構函數即可;第二個版本:接受一個迭代器區間,將區間範圍內的對象析構掉

空間的配置與釋放,alloc

考慮到小型區塊所可能造成的內存破損問題,SGI設計了雙層級配置器
分界點是配置的內存是否大於128B,大於就用第一級配置器直接使用malloc()和free(),小於等於則通過第二級訪問複雜的memory pool整理方式。

通過是否定義_USE_MALLOC宏,來設定是隻打開第一級還是同時打開第一級與第二級。SGI STL沒定義那個宏,也就是同時開放一、二級。

第一級配置器_malloc_alloc_template

一級空間配置器就是更大程度來合理運用空間,它的內部設計實際就是爲了壓榨剩餘的內存,達到內存的高效運用, 一級空間配置器內部其實就是mallocfree的封裝,然後儘量開闢想要的內存空間,==就算系統內部的剩餘內存空間小於你所申請的內存空間,==它都會努力嘗試開闢出來

👇
一級空間配置器的聲明:

/*__malloc_alloc_template*/
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;

一級配置器的重要函數有:allocate(開闢空間)reallocate(開闢空間)deallocate(釋放空間)

①首先看看allocate及相關函數的實現

第一配置器直接調用malloc(),當malloc分配失敗時,改用oom_malloc(), oom = out of memory, 處理內存不足的函數
關於oom_malloc函數:它的初值是定義爲0,即必須用戶自定義相應的內存不足處理函數才能執行,否則函數指針=0還是會拋出異常;
這個函數叫內存不足處理函數,在代碼中會定義對應的函數指針,該指針所指向的函數必須由用戶定義,因爲只有用戶知道哪些內存可以被釋放來騰出空間,如果沒有爲該函數指針賦予相應的函數,則此時直接會拋出bad_alloc異常,若該函數指針被指定,則會不停調用該函數,直到申請到足夠的內存,這裏把它叫做內存不足處理函數

體現在代碼中,oom函數是在一個無限循環中,退出循環的一個條件①就是沒有用戶自定義的內存不足處理函數,在if(0 == __my_malloc_handler)這個if語句裏面拋出異常;
第二個②退出循環條件就是在不斷調用用戶自定義內存不足函數之後,分配到了指定大小的內存,返回指向該內存區域的首地址

#define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)  
  
static void* allocate(size_t __n)  
{  
    void* __result = malloc(__n);      //調用malloc()分配內存,向 system heap 要求空間  
    if (0 == __result) __result = _S_oom_malloc(__n);     //如果malloc分配失敗,調用_S_oom_malloc()  
    return __result;                                      
}  
  
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG  
template <int __inst>  
void(*__malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;  
#endif                       
//內存不足處理例程,初值爲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;  
        /*由於初值設定爲0,如果用戶沒有自定義相應的內存不足處理例程,那麼還是拋出異常*/  
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }    
        (*__my_malloc_handler)();          //用戶有自定義(釋放內存),則進入相應的處理程序  
        __result = malloc(__n);              
        if (__result) return(__result);  
    }  
    //不斷的嘗試釋放和配置是因爲用戶不知道還需要釋放多少內存來滿足分配需求,只能逐步的釋放配置  
}  

但是這邊仍然會有一個問題或許可以考慮,就是即便有用戶自定義的內存不足處理函數,也不是一致調用它就可以不斷釋放空間,萬一極端情況,系統就是一點空間也釋放不出來了,那麼就真的會成爲一個死循環,所以其實可以進行防範以下這種情況,比如判斷一下本次函數有沒有運行清理出空間,如果有繼續循環,否則可能就可以拋出異常了

reallocate()函數

reallocate函數的內部運行過程和allocate函數的過程是相似的,只不過把malloc換成了realloc,oom_allocate換成了oom_reallocate,過程都是一樣的, (那爲什麼有了allocate還要有reallocate?)(一個是配置,一個是重配置,old_sz,new_sz)

static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)  
{  
    void* __result = realloc(__p, __new_sz); //reallocate 
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);  //獲取失敗,去oom
    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);  
    }  
}  

deallocate

就是簡單地封裝了一個free

static void deallocate(void* __p, size_t /* __n */)  
{  
    free(__p);        //第一級配置器直接使用free()  
}  

⭐:一級空間配置器以malloc()free()realloc()等C函數執行實際的內存配置,釋放,重配置的操作,因爲SGI不能直接使用C++的set_new_handler(), 我們就必須仿真出一個set_malloc_handler(),該函數接收一個返回值爲空,參數爲空的函數指針作爲參數,最後返回一個返回值和參數均爲空的函數

/*該函數接收一個返回值爲空,參數爲空的函數指針作爲參數,最後返回一個返回值和參數均爲空的函數指針*/  
static void (* __set_malloc_handler(void (*__f)()))()     
{  
    void (* __old)() = __malloc_alloc_oom_handler;       //保存原有處理例程  
    __malloc_alloc_oom_handler = __f;                    //重新指定異常處理例程  
    return(__old);  
}  

ps:分配內存時,如果每次new出來 ,都要判斷是否成功(地址是否爲空),比較繁瑣。
c++提供set_new_handler,當new失敗時,會調用set_new_handler設置的回調函數, 設置爲當前處理函數並返回原來的處理函數
set_malloc_handler()就是set_new_handler()的作用,就是用來處理內存不足準備的,它重新指定內存分配異常處理函數並返回原有的內存分配處理函數,爲什麼要保存並返回舊的處理函數,是方便以後又想使用;不斷調用這個內存不足處理函數,希望某次調用之後可以獲得我們申請大小的內存,正如一開始將的,這個函數中怎麼釋放空間是必須由自己定義方法的。

二級空間配置器__default_alloc_template 理論pa

why need 二級空間配置器:當頻繁地在內存中開闢出不連續的小塊內存時,會出現明明剩餘空間夠,但卻因爲剩餘空間不連續導致無法開闢出內存的現象,也就是所謂的外碎片問題;另一方面,大量的小區間也會使得操作系統用來記錄內存狀態的數據結構很臃腫

以上,第二級內存配置器所採取的策略是,在第一次申請小內存時,先申請一塊大內存留作備用,之後再申請小內存時,直接從上次申請的那一大塊內存中劃去要求的部分,不用再向系統申請。

同樣的,第二級空間配置器提供了標準接口allocate()deallocate()reallocate()三個接口,在開始這三個接口之前先對一些相關專業名詞瞭解一下:

  1. 內存區塊,有時也稱區塊
    內存區塊是指一塊小內存,它的大小均爲8的倍數,最大爲128Bytes,即有8、16、24、32、40、48、56、64、72、80、88、96、114、122、128這幾種,內存區塊有自己的首地址,可以存儲數據在每個區塊的前8個字節,存儲下一個可用區塊的地址,通過這種方式,可以形成一條區塊鏈表
  2. freelist數組
    內涵16個元素的數組,每一個元素是一個區塊鏈表的首指針
  3. 內存池
    內存池是是一大塊內存,它有三個參數:起始地址,終止地址以及大小,內存池的大小=終止地址 - 起始地址

在初始狀態下,內存池是空的,內存區塊也是不存在的,freelist數組中保存的都是空指針。我們從這種狀態下開始分析,該機制是如何運作的。

運行過程

當申請的內存大於128bytes時,直接轉交第一級配置器進行內存申請。

當申請的內存不大於128bytes時,則以內存池管理, 假設申請n字節:
1, 計算(n + 7)/7,得到一個整數值i,這個i即爲freelist的元素索引
2,訪問freelist位於i的元素,此時該元素爲NULL,不指向任何可用區塊(當區塊已經交給客戶端使用時, free-list就不再指向它們,即沒有可用區塊),這時將n向上調整爲8的倍數,並調用refill函數

//n                  調整爲8的倍數後申請的單個區塊的大小
//返回值       該區塊的地址
void* __default_alloc_template<threads,inst>::refill(size_t n);

3,refill的作用是給freelist重新填充內存區塊(?),這些區塊從內存池中獲取,一次默認取20個,通過函數chunk_alloc獲得

char* __default_alloc_template<threads,inst>::
chunk_alloc(size_t size , int& nobjs);
//size      申請的單個區塊的大小
//nobjs   注意,nobjs是一個引用類型
//在refill調用這個函數時,傳入的nobjs是20,即20個區塊
//chunk_alloc執行完成後,nobjs會被修改爲實際獲得的區塊數目

chunk_alloc函數返回的是一塊長度爲nobjs*n的內存塊,refill函數需要將這一整塊連續內存分割爲一個個內存區塊,並構建鏈表的連接關係

在內存充足的情況下,第一個內存塊會被返回給用戶使用,從第二塊內存塊開始構建鏈接關係;
在內存不足的情況下,假如只分配到了一個區塊,則該區塊直接交給用戶使用,freelist不進行更新;
如果不足20個,則仍將獲得的內存構建鏈接關係;
如果一個區塊都沒有獲得,因爲chunk_alloc函數內部調用了第一級配置器填充內存池,因此會按照第一級內存配置器的方式處理內存不足的情況。

chunk_alloc函數 P67~68⭐

char* __default_alloc_template<threads,inst>::
chunk_alloc(size_t size , int& nobjs);

需要關注的幾個參數:
1,申請的內存總大小,size*nobjs,用total_bytes來表示
2,內存剩餘空間,用bytes_left表示

如果total_bytes小於bytes_left,則直接划走total_bytes這麼多內存,同時更新內存池的狀態;

如果內存池的剩餘空間不夠申請的那麼多區塊,只夠供應一部分區塊,那麼計算最多能劃多少塊,並划走;

如果連一個區塊都無法供應,這時候就要給內存池“加水”:

  1. 首先要把內存池中剩下的水收集起來,別浪費了,加到freelist上去,具體的步驟是,根據剩下的內存的大小確定freelist的index,因爲每個內存塊都是8的倍數,划走時也按照8的倍數劃分的,因此剩下來的內存一定可以構成一個內存區塊,找到合適的freelist位置後,將這個區塊加到freelist上,這時,就可以開始“加水”了
  2. 加水:利用malloc從heap中配置內存,爲內存池注水以應付需求,且新水量爲需求的兩位,再加上一個隨着配置次數增加而愈加增大的附加量(?)
    SGI STL選擇的量是: 2 × total_bytes + heap_size >> 4 heap_size是以往內存池的容量的累加和,即附加量要滿足,隨着“加水”次數變多,每次加水的量應該越來越大這個條件
    3.確定加多少水後,通過malloc函數獲取內存:
    ①如果獲取成功,則更新內存池的狀態,並遞歸調用chunk_malloc,因爲內存池已經充足,下一次能夠直接獲取指定的內存
    ②如果沒能獲取那麼多內存,首先,遍歷freelist,如果freelist裏面有大小大於一個size的空閒區塊,則將這個區塊加入到內存池,並遞歸,
    🌂注意,這裏的遍歷並不是那種從freelist第一個開始逐個檢查,而是以size爲起點,確定freelist中相應的index,如果該index不含有空閒區塊,則將size增加8字節,也就是檢查下個freelist,直到後面的freelist都檢查完,中途找到任何一個空閒區塊,都會立即返回,不再遍歷;
    ③如果遍歷freelist也找不到足夠的空閒區塊,那麼只能指望第一級配置器中由用戶設置的內存不足處理函數能否解決,這裏轉交給第一級空間配置器,這時,要麼第一級空間配置器順利獲得內存,這時會更新內存池,並遞歸,沒能順利獲得內存,則會拋出異常。

釋放內存

釋放內存的過程相對簡單,由第二級內存配置器分配的內存,在釋放時並不交由free函數進行釋放,也不放到內存池中,而是把內存加入到freelist鏈表中,以備下次使用,這個過程主要是簡單的鏈表操作

內存區塊鏈表 freelist

freelist中,每一個元素都是obj*,obj的結構👇

union obj
{
          union obj* free_list_link;//union
          char client_data[1];
}

它是內存區塊的鏈表節點,需要記錄當前區塊的地址,以及下一個區塊的地址,每個地址都是8個字節的指針,用一個struct來表示,需要16字節,而使用union結構,只需要8字節(?)

在每個內存區塊的前8個字節處,是個obj對象,它存儲着下一個內存區塊的地址,有效區間爲這8個字節,client_data是一個長度爲1的數組,只有一個元素,它就是內存區塊的第一個字節,爲這個字節定義一個變量,並對它取址,得到的就是當前區塊的地址。

這裏採用數組的形式而不是直接定義一個char,目的是直接將client_data作爲數組首地址返回,而不需要調用取址運算符,將該內存區塊返回時,返回client_data,無須進行類型轉換,直接在union中切換就行,狀態的改變不會改變前8個字節的內容,但內存區塊交出去後,前八個字節的內容丟失也不重要了,在將內存區塊加入到freelist中時,會重新設置前8個字節的值,保證數據的有效性。
(爲了維護鏈表,每個節點都需要額外的指針,爲了解決這種負擔,用union:當節點所指的內存塊是空閒塊,obj被視爲一個指針,指向另一個節點,當節點已經被分配時,被視爲一個指針指向實際區塊)

總結一下,二級配置器的運行

在這裏插入圖片描述

  1. 如果想申請32個bytes時,找到free_list的3號下標,從裏面拿掉第一個內存塊返回給用戶,然後讓下標元素的值指向拿走的內存塊的下一個內存塊,如果是空,那麼沒有內存可用。
  2. 如果想申請24bytes,找到2號下標,但是2號下標下並沒有內存塊,這時候系統就會直接分20個對應大小的內存塊,全部掛到這個下標位置,當下次申請該大小內存就和情況1一樣
  3. 如果想申請25bytes,就申請32bytes(內碎片)

二級空間配置器__default_alloc_template 代碼pa

二級空間配置器主要函數聲明和具體框架

①定義全局的小型區塊的上調邊界和小型區塊的上限以及free_list的個數
②round_up():將所申請的字節數上調爲8的倍數
③free_list的節點構造
④配置一大塊空間的refill(),可容納nobjs個大小爲size的區塊
⑤chunk_alloc()函數,內存池的起始位置和結束位置都只在chunk_alloc()中改變
⑥allocate()、deallocate()、reaoolocate()
⑦最後是static data member的定義

template <bool threads, int inst>  
class __default_alloc_template {  
  
private:  
  //effective C++ 中條款二:  儘量使用const enum inline替換#define.
# ifndef __SUNPRO_CC  
    enum {__ALIGN = 8};  
    enum {__MAX_BYTES = 128};  
    enum {__NFREELISTS = __MAX_BYTES/__ALIGN};  
# endif  
  static size_t ROUND_UP(size_t bytes) {  
        return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));  
  }  
__PRIVATE:  
  union obj {  
        union obj * free_list_link;  
        char client_data[1];    /* The client sees this. */  
  };  
private:  
# ifdef __SUNPRO_CC  
    static obj * __VOLATILE free_list[];   
        // Specifying a size results in duplicate def for 4.1  
# else  
    static obj * __VOLATILE free_list[__NFREELISTS];   
# endif  
  static  size_t FREELIST_INDEX(size_t bytes) {  
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);  
  }  
  
  // Returns an object of size n, and optionally adds to size n free list.  
  static void *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 *chunk_alloc(size_t size, int &nobjs);  
  
  // Chunk allocation state.  
  static char *start_free;  
  static char *end_free;  
  static size_t heap_size;  
  
 /* 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>::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[  
# ifdef __SUNPRO_CC  
    __NFREELISTS  
# else  
    __default_alloc_template<threads, inst>::__NFREELISTS  
# endif  

allocate函數的源碼

如果申請數大於128則調用一級空間配置器,防止線程資源競爭要定義個鎖
定位到我們想申請的在free_list上的位置,如果對應的內存大小的下標位置有內存塊的話直接拿走內存塊,沒有的話就要refill去申請
放上之後要更新鏈表的連接狀態

static void * allocate(size_t n)
{
	obj * __VOLATILE * my_free_list; //遇到VOLATILE關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問
	obj * __RESTRICT result;
 
	if (n > (size_t)__MAX_BYTES) {
		return(malloc_alloc::allocate(n)); //調用一級空間配置器
	}
 
	my_free_list = free_list + FREELIST_INDEX(n);
	//FREELIST_INDEX(n)函數是用來獲取free_list下標的,一會會提到的.
#       ifndef _NOTHREADS
	
	//防止線程資源競爭,加鎖.
	lock lock_instance;
#       endif
 
	//這個過程是將頭部的內存空間返回給用戶,然後讓下標元素的值指向下面的內存塊
	//類似鏈表的頭刪過程,不過是將拿掉的內存塊返回給用戶了.
	//refill(ROUND_UP(n)) 是用來向系統內存申請空間的.
	//ROUND_UP(n) 這個函數是爲了讓n和8的倍數對齊. 申請空間單個的單位一定是free_list中對應的內存塊大小.
	result = *my_free_list;
	if (result == 0) {
		void *r = refill(ROUND_UP(n));
		return r;
	}
	*my_free_list = result->free_list_link;
	return (result);
};

refill()函數源碼

如果對應大小下標位置沒有一個內存塊時就需要調用refill函數向系統申請內存

每次refill的時候,二級空間配置器希望直接向內存申請20塊相同大小的內存塊,這樣就不用反覆的調用自己;爲了提高效率,定一個nobjs,將它傳給chunk_alloc函數,具體申請空間就讓chunk_alloc函數去做,refill函數就專心做鏈接內存塊的事情

如果chunk_alloc函數只申請到一個內存塊,那麼直接返回;如果不止一個,那麼要進行內存鏈接,第一個區塊是直接返回給用戶的

template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
	int nobjs = 20;
	char * chunk = chunk_alloc(n, nobjs);//申請20個n大小的內存塊
	obj * __VOLATILE * my_free_list;
	obj * result;
	obj * current_obj, *next_obj;//用於連接
	int i;
 
	//如果chunk_alloc函數只申請到一個內存塊,那麼直接返回.
	if (1 == nobjs) return(chunk);
 
	//chunk_alloc申請的內存塊不止一個,可以進行鏈接內存塊.
	my_free_list = free_list + FREELIST_INDEX(n);
 
	//下面的過程是鏈接內存塊的過程.
	//這裏的my_free_list和next_obj 都是直接指向chunk+n的位置的. 因爲第一個內存塊需要返回給用戶使用.
	result = (obj *)chunk;
	*my_free_list = next_obj = (obj *)(chunk + n);
 
	for (i = 1;; i++) { //i=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);
}

把連接內存塊的過程摘出來看看:

		my_free_list = free_list + FREELIST_INDEX(n);//收穫了不止一個區塊,要做調整納入新節點
	result = (obj *)chunk;//這是第一塊內存地址,要返回的結果
	*my_free_list = next_obj = (obj *)(chunk + n);//將free_list指向新配置的空間, n是每塊內存塊的字節大小
 
 //👇第1個返回之後,從第二個開始進行連接並更新指針,之後又要取內存塊的話是從當前的第二個開始的
	for (i = 1;; i++) { //i=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);
}

其中ROUND_UP將n保證爲8的倍數並且這個數大於等於n;FREELIST_INDEX在free_list中找到相應的下標

static size_t ROUND_UP(size_t bytes) {
		return (((bytes)+__ALIGN - 1) & ~(__ALIGN - 1));
	}
static  size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

chunk_alloc源碼

當freelist中需要的對應內存大小下標位置沒有一個內存塊時就需要向系統申請內存,我們知道用的時refill()但實際的從內存池中取具體空間是chunk_alloc函數去做的
那麼關於具體申請空間:
①當系統剩餘的內存大於希望申請的20個相同內存塊空間時,就直接划走
②當系統中剩餘空間不能完全滿足需求量,但足夠供應一個(或大於一個)以上的區塊,就能划走多少划走多少,更新返回的節點數就ok
③如果內存池連一個區塊都無法給客戶端提供,那麼就得用malloc申請內存,申請2倍需求量+附加量的內存;在申請之前先將內存池中的一些殘餘零頭加入到free_list中,利用FREELIST_INDEX()找到適當的free list,加入它並調整連接
④如果heap也沒有內存了配置失敗,那就回到free_list中尋找是否有大於單個區塊的內存塊還沒有使用,如果有的話就釋放內存,再掛到申請的區塊位置上
⑤還是失敗的話,就調用一級空間配置器,嘗試用oom機制

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;
 
	//當系統中剩餘的內存大於你申請20個相同內存塊的空間.
	if (bytes_left >= total_bytes) {
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
	//當系統中剩餘的內存大於一個你申請的內存塊.
	else if (bytes_left >= size) {
		nobjs = bytes_left / size;
		total_bytes = size * nobjs;
		result = start_free;
		start_free += total_bytes;
		return(result);
	}
 
	//
	else {
		//如果內存池連一個區塊都無法給客端提供,就調用malloc申請內存,新申請的空間是需求量的兩倍
		//與隨着配置次數增加的附加量. 在申請之前,將內存池的殘餘的內存收回.
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		if (bytes_left > 0) {
			obj * __VOLATILE * my_free_list =
				free_list + FREELIST_INDEX(bytes_left);
 
			((obj *)start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *)start_free;
		}
		
		
		start_free = (char *)malloc(bytes_to_get);//配置heap空間,用來補充內存池
		 //if (0 == start_free)
		//如果沒有申請成功,如果free_list當中有比n大的內存塊,這個時候將free_list中的內存塊釋放出來.
		//然後將這些內存編入自己的free_list的下標當中.調整nobjs.
		if (0 == start_free) {
			int i;
			obj * __VOLATILE * my_free_list, *p;
 
			for (i = size; i <= __MAX_BYTES; i += __ALIGN) {//從size開始找
				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));
					// Any leftover piece will eventually make it to the
					// right free list.
				}
			}
			end_free = 0;	// In case of exception.
 
			//這個是個就是彈盡糧絕了,去求求一級空間配置器來幫幫忙. 看看oom機制是否能過來幫忙
			//這樣會有兩種可能,一種是拋出ban_alloc異常 一種是開闢空間成功.
			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()釋放空間源碼

  1. 如果需要回收的區塊大於128bytes,則調用第一級空間配置器
  2. 如果需要回收的區塊小於128bytes,則找到free_list當中該塊空間的大小的位置,然後將區塊回收. (這裏的回收其實就是頭插):還是用FREELIST_INDEX()找到屬於哪個區塊,然後頭茬
/* p cannot be 0 */
static void deallocate(void *p, size_t n)
{
	obj *q = (obj *)p;
	obj * __VOLATILE * my_free_list;
 
	if (n > (size_t)__MAX_BYTES) {//大於128, 去一級
		malloc_alloc::deallocate(p, n);
		return;
	}
	my_free_list = free_list + FREELIST_INDEX(n);
	// acquire lock
#       ifndef _NOTHREADS
	/*REFERENCED*/
	lock lock_instance;
#       endif /* _NOTHREADS */
 
	//很標準的一個頭插
	q->free_list_link = *my_free_list;
	*my_free_list = q;
	// lock is released here
}

總結❗

在這裏插入圖片描述

二級空間配置器的缺陷與問題

可參考https://blog.csdn.net/dawn_sf/article/details/78774275

  1. 二級空間配置器從頭到尾都沒有看到它釋放內存,究竟是否釋放,合適釋放:
    答:二級配置器並沒有將申請的空間釋放,而是將它們掛在了自由鏈表上,空間配置器的所有方法,成員都是靜態的, 那麼它們就存放在靜態區,因此釋放的實際必定也是程序結束時

  2. 自由鏈表釋放空間的連續性問題
    真正在程序中就歸還空間的只有自由鏈表中的未使用值,由於用戶申請空間、釋放空間順序的不可控性,這些空間並不一定是連續的,而釋放空間必須保證其連續性。保證連續的方案可以是:跟蹤分配釋放過程、記錄節點信息,釋放時,僅釋放連續的大塊空間。(倒是沒咋看懂)

  3. 二級空間配置器的效率問題
    二級配置器雖然解決了外碎片的問題,但同時也造成了內存片。如果用戶頻繁申請char類型的空間,而配置器默認對其到8的倍數,那麼剩下的7/8的空間就會浪費。如果用戶頻繁申請8字節的空間,甚至將堆中的可用空間全部掛在了自由鏈表的第一個節點,這時如果申請一個16字節的內存,也會失敗。這也是二級配置器的弊端所在,設置一個釋放內存的函數是很有必要的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章