二級空間配置器的原理剖析和簡單實現

首先來講一下頻繁地向系統申請小內存塊的缺點

 用戶代碼與操作都是在用戶態,而操作系統是屬於內核態的,用戶在向系統申請空間的時候是通過操作系統來申請的,所以,每一次空間申請就會進行用戶態與內核態之間的切換,會大大降低效率。

       系統在將一塊空間交給用戶去使用的時候,並不是就將這塊空間交給用戶就行了,這塊空間是可讀的還是可寫的,有多大等相關信息都要告知用戶,所以,還會有額外的開銷,每一塊內存都會分配額外的空間保存這樣的信息,可能所需內存就4個字節,而保存這些信息卻用了40個字節。這樣就很浪費空間

 

       第二級空間配置器就是爲了避免上述的問題而存在的。在SGI中,以128字節爲分界線,如果申請的空間大於128字節就算是申請大的內存塊,需要使用一級空間配置器來給予分配,如果申請的空間小於等於128字節就算做是小內存塊,則使用二級空間配置器來分配。爲了避免頻繁地向系統索要一塊塊小的空間,二級空間配置器通過內存池的方式來管理各個小空間,也就是一次性向系統申請一塊比較大的空間,放到內存池裏邊,然後,之後在申請小的內存塊的時候就不是向系統要了,而是由內存池來分配。

       這樣做有什麼好處呢?操作系統有內核態和用戶態,系統空間管理是在內核態,我們用戶的行爲都是屬於用戶態的,而用戶態與內核態之間的切換開銷是非常大的,使用內存池的方式,每次申請空間都向內存池(在用戶態)申請,這樣就大大減少內核態與用戶態的切換的次數以及省去爲了管理這塊空間而產生的額外的開銷等,它的作用就不言而喻了。當空間使用完,再還給內存池,如果內存池裏邊的空間都不使用了,再由內存池同一歸還給系統。

       內存池具體是怎樣管理空間的呢?

假如我們向系統裏邊申請了128個字節放到內存池裏邊,爲了方便對這塊空間的管理 ,我們可以使用一個start指針和一個end指針將這塊空間的起始地址和結束地址都保存起來。然後每分配幾個字節,start指針就往後挪幾個字節,直到start指針與end指針指向同一個位置,說明內存池裏邊的空間已經全部分配出去了。對於分配的管理還是很簡單的,那麼,釋放的時候呢?

      現在,程序A申請了4個字節,然後程序B申請了6個字節,程序C申請了3個字節,程序A又申請了15個字節。現在,程序C的空間不需要了,於是將它還給內存池,那麼,start指針應該指向什麼地方呢?有兩種選擇:

    1、程序C的空間的起始處,這樣會導致後邊已經分配出去但還沒有還回來的空間被當做已經歸還,並有可能再將其分配給其他程序,在本例子中就是程序A又申請的15個字節會被當做是已經歸還(事實上程序A還正在使用這塊空間),如果,下一次來了個程序D,向內存池裏邊申請了30個字節,那麼,這就出問題了,程序A和程序D的空間有重疊。

    2、分配給程序C的空間爲3個字節,那麼,start指針往前移3個字節。但是這樣同樣也會導致程序A沒有歸還的空間被當做已經歸還的空間被再次分配,而本來要歸還的空間並沒有放入到內存池裏邊。

很顯然,上邊這兩種方法都行不通,行不通的原因就在於我們試圖用一個指針去標識很多塊不連續的空間,說到不連續的空間,自然而然地,就應該想到鏈表,我們可以使用鏈表的結構來標識這些被分配出去然後又還回來的離散的空間。那麼,我們可以使用類似於哈希桶這樣的結構來管理這些空間,定義一個數組,數組元素裏邊存放的就是某塊空間的地址,只要分配出去後又還回來的空間都把他掛到對應的“哈希桶”中。如下圖:

下次需要申請空間的時候就可以去內存池裏邊申請也可以直接從“哈希桶”裏邊直接申請。這樣雖然解決了空間回收的問題,但是有引入了新的問題。

假設有這樣一種場景,我想要申請一個15字節的空間,但是內存池利裏邊只剩下12字節的空間了,而15字節空降大小對應的“哈希桶”裏邊也沒有空間結點,所以只能往後找一塊比15字節空間大的空間(假設找到的是16字節的空間)切割下15字節的空間,然後將剩下的1個字節的空間掛到對應的“哈希桶”裏邊,雖然這一次空間申請得到了滿足,但是切割剩下的空間一般都會很小,如果類似的情況發生很多次,那麼1個或者2個字節的小空間會有很多個,而我們都知道,這樣的小空間基本都派不上用場,這樣就導致了“內存碎片”的問題,很是浪費空間。

       可謂是魔高一尺道高一丈,爲了解決(準確的說是減緩)上邊的問題,SGI第二級空間配置器使用一個很巧秒的方法來分配內存。

接下來的講的就是SGI二級空間配置器的處理方式了-------重點來了~

       SGI對空間的分配與回收就是以“哈希桶”的方式,不過在此並沒有把它就做“哈希桶”,而是自由鏈表(free_lists),爲了方便管理,SGI二級配置器會主動將任何小額區塊的內存需求量上調至8的整數倍,所以自由鏈表中管理的空間大小爲分別爲(8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 字節)

       這樣管理空間有什麼好處呢?我們在申請空間的時候,並不是我們申請多少字節的空間,它就分配多少空間,有可能分配給我們的空間要大一些,比如說我們申請5個字節,就會被對齊到8個字節,如果free_lists裏邊對應的位置管理的有8個字節的空間,那麼,它會將8個字節的空間分配給我們,還回去的時候就將這8個字節一塊還回去就行了;如果對應位置沒有的話,就會向後邊找,找到16個字節的空間,分配給我們8個,剩下的8個字節的空間就會被放到0號位置管理起來,最由於每次都會對齊到8個字節的空間,雖然分配出去的空間中可能會浪費少量的幾個字節,但是這樣不會將空間分割的很碎,最小也是8個字節的空間,8個字節的空間是可能被使用的,這就增加了空間的複用率

       說了那麼久的free_lists,現在來看看這個free_lists的結構:

       可能會有讀者覺得很奇怪,爲什麼使用的是聯合體呢?我們在實現一個鏈表的時候,都是使用一個結構體,結構體裏邊都是數據域加指針域。這裏爲了將free_lists中管理的各個空間結點鏈接起來,每個結點勢必需要一個指針來鏈接下一塊空間。對於二級空間配置器來說,在free_lists中只需要將空間管理起來不需要存放數據,所以只要有一個指針就可以了。對於用戶來說,不需要關心空間的管理,而是直接往空間裏存放自己的數據就行了,所以將obj的結構設置成一個聯合體,聯合體是所有成員共用一塊空間的。這樣就可以實現一物二用,不用爲了維護鏈表所必須的指針而造成另一種浪費。

       接下來介紹一下每一步的實現,主要是介紹分配空間的函數的實現。

前邊說到了,SGI空間配置器分兩級,大於128字節的使用一級空間配置器,小於等於128字節的使用二級空間配置器。那麼,空間分配函數第一步要乾的事就是判斷申請空間的大小,然後根據大小來確定分配空間的方式(使用一級空間配置器還是二級空間配置器的方式),在這裏只討論使用二級空間配置器的情況,即申請的字節數小於128。

下邊以申請12字節的空間爲例講解一下二級空間配置器處理空間申請的過程-------12字節會被上調到8的整數倍,也就是16.

       首先去對應自由鏈表裏邊找(通過字節數16對齊到計算出對應位置爲1號自由鏈表),看自由鏈表對應位置有沒有空間,如果有,就採用頭刪的方式將自由鏈表上掛接的第一個空間結點分配給用戶使用,然後將剩下的空間繼續掛接起來空間申請就結束了,簡單又高效。但是如果0號自由鏈表中沒有空間,就需要去內存池裏邊申請了,如果就向內存池申請一塊 16個字節的空間,下一次申請16字節空間的時候自由鏈表中依然是沒有空間,又要到內存池裏邊申請,每次都這樣,很麻煩,所以,在向內存池裏申請空間的時候,不是一次只申請一塊16個字節的空間,而是多申請幾塊16字節的空間(比如20塊),方便下一次 16字節空間的申請。

       到內存池裏邊申請空間,又有三種情況,第一種情況:內存池裏邊空間還很足,16字節大小的空間還可以分配出20塊,那麼,將其中1塊分配給用戶使用,然後將剩下的19塊掛接到自由鏈表中,下一次申請16字節的空間就可以直接在自由鏈表中取了。第二種情況:內存池裏邊空間不是很足,不能一次性提供 20 個塊 16個字節的空間,但是可以提供5塊 16字節的空間(不一定是5 ,只要比20小,大於等於1就行),那麼,就將能提供的這5塊空間的其中一塊返回給用戶使用,剩下的4塊掛接到自由鏈表中;第三種情況:內存池裏的空間已經很緊張了,假如只剩下8個字節,連一塊16字節的空間都提供不了了,那麼這個時候就需要朝系統去申請空間來填充內存池了,由於管理內存池的就是一個_start  和 _end 指針,只能標記上一塊連續的空間,所以,之前內存池中還剩的8個字節的空間就需要將它掛接到對應的自由鏈表(也就是0號自由鏈表)中,然後向系統申請一端空間放入到內存池中,並用_start  和 _end 指針將這段空間標記管理起來。這就回到了第一種情況。

       向系統申請一大塊空間就一定能申請成功麼?不一定,如果系統裏邊的空間也已經不足了就會申請失敗,這個時候,就需要向自由鏈表後邊管理的比16字節大的空間裏邊去切割了,因爲不確定那個自由鏈表裏有空間,所以,需要遍歷管理自由鏈表的數組,在遍歷的時候有一個小細節,目前我們申請的16字節的空間對應的是1號自由鏈表,按理說我們應該從2號自由鏈表開始查找,但是由於需要考慮多線程,(有可能在我們向內存池或系統申請空間的時候,另一個線程已經歸還空間並掛接到了1號自由鏈表中),所以,依然從1號自由鏈表開始遍歷。當遍歷到某個自由鏈表(假如是3號自由鏈表中管理的有空間),那麼,就從3號自由鏈表中取一個空間結點下來(同樣是以頭刪的方式)放入到內存池中,然後再使用在內存中申請空間的方式申請空間就OK了

       如果一直將後邊全部的自由鏈表都遍歷完了,也沒有找到一塊空間。也就是說所有自由鏈表、內存池、系統裏都不能提供空間,可以說是山窮水盡了。空間都去哪了?已經分配給用戶了,那麼,只能調用一級空間配置器,因爲一直空間配置器在無法分配空間的時候會調用用戶設置的內存釋放函數,將自己已經不用的空間歸還給系統,這樣就有可能還能申請到空間,如果用戶沒有設置內存釋放函數的話,那麼申請空間的結果就只能是失敗(拋異常)了。

代碼實現:

代碼中包含一級空間配置器的實現,爲了方便調試,在二級空間配置器的實現中添加了一些打印信息

#pragma once
#include <new>
#include <iostream>
using namespace std;

/**********************************************************************/
//一級空間配置器的實現
#define THROW_BAD_ALLOC cerr<<"out of memory"<<endl;exit(1)

template <int inst>
class Alloc_malloc
{
public:
	//Allocate用於申請空間
	static void* Allocate(size_t n)
	{
		void* result = malloc(n);
		//申請失敗,使用oom_malloc()重新嘗試申請
		if (result == NULL)
			result = Oom_malloc(n);
		return result;
	}

	//Deallocate用於釋放空間
	static void Deallocate(void* p, size_t /* size*/)
	{
		free(p);
	}

	//Reallocate用於根據需要調整已經存在的空間的大小
	static void* Reallocate(void* p, size_t size)
	{
		void* result = realloc(p, size);
		//申請失敗,使用oom_realloc()嘗試申請
		if (result == NULL)
			result = Oom_realloc(p, size);
		return result;
	}

	//Set_malloc_handler函數是用於設置用戶提供的釋放空間的函數指針
	static void(*Set_malloc_handler(void(*f)())) ()
	{
		void(*old)() = _Malloc_alloc_oom_handler;
		_Malloc_alloc_oom_handler = f;
		return (old);
	}

private:
	//通過用戶提供的釋放空間(釋放自己已經不用了的空間)的函數不斷的釋放空間並檢測
	//直到釋放出的空間足夠分配給申請的空間
	//如果用戶沒有提供釋放空間的函數,則拋異常
	static void* Oom_malloc(size_t size)
	{
		void* result;
		void(*My_malloc_handler)();
		for (;;)
		{
			My_malloc_handler = _Malloc_alloc_oom_handler;
			if (0 == My_malloc_handler)//用戶沒有提供釋放空間的函數
				THROW_BAD_ALLOC;
            _Malloc_alloc_oom_handler();
			result = malloc(size);
			if (result)
				return result;
		}
	}

	static void* Oom_realloc(void* p, size_t size)
	{
		void* result;
		void(*my_malloc_handler) ();
		for (;;)
		{
			my_malloc_handler = _Malloc_alloc_oom_handler;
			if (0 == my_malloc_handler)
				THROW_BAD_ALLOC;
            _Malloc_alloc_oom_handler();
			result = realloc(p, size);
			if (result)
				return result;
		}
	}

private:
	static void(*_Malloc_alloc_oom_handler)();
};

//類外初始化靜態成員變量 _malloc_alloc_oom_handler
template <int inst>
void(* Alloc_malloc<inst>::_Malloc_alloc_oom_handler)() = 0;

/***********************二級空間配置器的實現******************************/

enum{ALINE = 8}; //每次分配的最小空間的大小
enum {MAX_BYTES = 128}; //二級空間配置器最大分配的空間大小
enum {NFREELISTS = MAX_BYTES/ALINE}; //free_lists的個數

template <int inst>
class default_Alloc
{
public:
	static void* Allocate(size_t size)//開闢空間
	{
		obj* volatile result;
		//大空間,使用一級空間配置器分配內存
		printf("申請%d字節的空間\n", size);
		if (size > MAX_BYTES)
		{
			printf("申請空間大於128字節,使用一級空間配置器\n");
			return Alloc_malloc<inst>::Allocate(size);
		}
			
		
		//小空間,使用二級空間配置器分配內存
		//先去對應的自由鏈表中找
		int index = Free_list_index(size);
		result = _free_list[index];
		if (NULL == result)//自由鏈表中沒有空間,從內存池裏申請空間
		{
			printf("第%d號自由鏈表中沒有空間,向內存池中申請空間\n", index);
			result = (obj*)Refill(size);
			return result;
		}
		_free_list[index] = result->free_list_link;
		printf("在第%d號自由鏈表中獲取到%d個字節的空間\n", index, size);
		return result;	
	}

	static void Deallocate(void* p, size_t n)//釋放空間
	{
		if (n > 128)
		{
			printf("釋放%d個字節的空間,歸還給系統\n", n);
			Alloc_malloc<inst>::Deallocate(p, n);
			return;
		}
		obj* pCur = (obj*)p;
		int index = Free_list_index(n);
		pCur->free_list_link = _free_list[index];
		_free_list[index] = pCur;
		printf("釋放%d個字節的空間,掛接到%d號自由鏈表中\n\n", n, index);
	}

private:
	static size_t Round_up(size_t bytes)//用於上調至ALINE的整數倍
	{
		return (((bytes)+ALINE - 1)&~(ALINE - 1));
	}

	static size_t Free_list_index(size_t bytes)//用於計算在free_lists中的下標位置
	{
		return (bytes - 1) / ALINE;
		//return (bytes+(ALINE-1))/ALINE-1;  //SGI中的處理方式
	}
	static void* Refill(size_t bytes) //向內存池中申請空間
	{
		int nobjs = 20;
		//調用chunk_alloc函數,嘗試獲取nobjs個內存塊
		char* chunk = Chunk_alloc(bytes, nobjs);
		obj* result;
		if (1 == nobjs)//只申請到一塊空間的大小,將該塊空間返回給用戶使用
		{
			printf("從內存池裏成功申請到%d個%d字節大小的內存塊\n", nobjs, bytes);
			return chunk;
		}
		
		//否則代表有多塊,將其中一塊返回給用,其他的掛接到自由鏈表中
		printf("從內存池裏成功申請到%d個%d字節大小的內存塊\n", nobjs, bytes);
		result = (obj*)chunk;
		int n = Free_list_index(bytes);
		obj* next = (obj*)(chunk + bytes);
		while (1)
		{
			next->free_list_link = _free_list[n];
			_free_list[n];
			next = (obj*)((char*)next + bytes);
			if ((char*)next == chunk + nobjs*bytes)
			{
				printf("成功將剩下的%d個%d字節大小的內存塊掛接到%d號自由鏈表中\n", nobjs - 1, bytes, n);
				break;
			}
				
		}
		return result;
	}


private:
	static char* Chunk_alloc(size_t size, int& nobjs)
	{
		char* result;
		size_t total_bytes = size*nobjs;
		size_t bytes_left = _end - _start;
		if (bytes_left >= total_bytes)//內存池裏還有足夠的空間
		{
			printf("向內存池申請%d個%d字節大小的內存塊\n", nobjs,size);
			result = _start;
			_start += total_bytes;
			return result;
		}
		else if (bytes_left >= size)//內存池裏邊還能提供至少一塊內存塊
		{
			nobjs = bytes_left / size;
			result = _start;
			_start += nobjs*size;
			printf("向內存池申請%d個%d字節大小的內存塊\n", nobjs, size);
			return result;
		}
		else
		{
			//內存池裏邊連一個內存塊空間的大小都沒有,朝系統裏邊申請空間填充內存池
			if (bytes_left > 0)//如果內存池裏還剩有空間
			{
				int n = Free_list_index(bytes_left);
				((obj*)_start)->free_list_link = _free_list[n];
				_free_list[n] = (obj*)_start;
			}
			size_t bytetoget = 2 * total_bytes + Round_up(_heap_size >> 4);
			_start = (char*)malloc(bytetoget);
			printf("內存池空間不足%d字節,向系統申請%d字節大小的內存塊\n", size, bytetoget);
			if (NULL == _start)//系統裏邊沒有足夠的空間
			{
				printf("系統空間不足,不能分配%d字節大小的內存塊\n", bytetoget);
				//從當前自由鏈表開始遍歷管理自由鏈表的數組
				for (size_t i = size; i <= MAX_BYTES; i++)
				{
					int n = Free_list_index(i);
					if (_free_list[n])//該自由鏈表裏管理的有空間
					{
						_start = (char*)_free_list[n];
						_end = _start + i;
						printf("在第%d號自由鏈表中獲取到%d個字節的空間放入內存池中\n", n, size);
						//遞歸調用自己,方便調整nobjs
						return (Chunk_alloc(size, nobjs));
					}
				}
				//已經是山窮水盡了,嘗試調用一級空間配置器來申請
				printf("已經山窮水盡了,使用一級空間配置器來再次嘗試申請空間\n");
				_end = 0;
				_start = (char*)Alloc_malloc<inst>::Allocate(size);
			}
			_end = _start + bytetoget;
			_heap_size += bytetoget;
			//遞歸調用自己,方便調整nobjs
			return (Chunk_alloc(size, nobjs));
		}
	}

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

private:
	static char* _start;
	static char* _end;
	static size_t _heap_size;

	static obj* volatile _free_list[NFREELISTS];
};

//類外初始化類的靜態成員變量
template <int inst>
char* default_Alloc<inst> ::_start = 0;

template <int inst>
char* default_Alloc<inst> ::_end = 0;

template <int inst>
size_t default_Alloc<inst> ::_heap_size = 0;

template <int inst>
typename default_Alloc<inst>::obj* volatile default_Alloc<inst>::_free_list[NFREELISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };


/*****************************測試部分*********************************/
//測試一級空間配置器
void test1()
{
	int* p = (int*)Alloc_malloc<0>::Allocate(3 * sizeof(int));
	p[0] = 0;
	p[1] = 1;
	p[2] = 2;
	p = (int*)Alloc_malloc<0>::Reallocate(p, 5 * sizeof(int));
	p[3] = 3;
	p[4] = 4;

	for (int i = 0; i<5; i++)
	{
		cout << p[i] << " ";
	}
	cout << endl;
}

void test2()
{
	int* p1 = (int*)default_Alloc<0>::Allocate(8);
	default_Alloc<0>::Deallocate(p1, 8);

	int* p = (int*)default_Alloc<0>::Allocate(8);
	default_Alloc<0>::Deallocate(p, 8);

	int* p2 = (int*)default_Alloc<0>::Allocate(20);
	default_Alloc<0>::Deallocate(p2, 20);

	int* p3 = (int*)default_Alloc<0>::Allocate(200);
	default_Alloc<0>::Deallocate(p1, 200);
}

結果顯示

 

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