007 高并发内存池_回收内存

​🌈个人主页:Fan_558
🔥 系列专栏:项目一
☀ 代码仓库:高并发内存池
🌹关注我💪🏻带你学更多操作系统知识

在这里插入图片描述

前言

本文将会向你介绍ThreadCache、CentralCache、PageCache是如何回收内存的
在这里插入图片描述

一、ThreadCache回收内存

1、当某个线程肾申请的内存对象不再使用了,就会释放到threadcache中,然后threadcache将内存对象插入到对应的哈希桶的自由链表中
2、线程不断地释放内存对象,最终在某个哈希桶中的自由链表的长度会越来越长,每一个线程都有一个独立的threadcache,若是不及时返还给centralcache,会造成浪费
3、当线程二向centralcache申请的时候,若是线程一占有太多空间资源了,导致线程二向centralcache申请,然后centralcache还需要向下一层pagecache进行申请
4、因此当一个线程中在threadcache所闲置的内存对象过多,我们应该将释放到的内存对象返还给central cache

//释放pTLSThreadCache对象(需要告诉释放哪一个桶)
《ThreadCache.cpp》
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size < MAX_BYTES);
	//计算出属于哪一个桶
	size_t index = AlignmentRules::Index(size);
	//用自由链表管理释放的内存
	_freeLists[index].Push(ptr);
	//当链表长度大于一次批量申请的内存时就开始还一段list给central cache
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

当自由链表的长度大于或等于threadcache一次批量向centralcache申请的内存对象个数,此时就需要把该自由链表中的对象返还给centralcache中对应的span

《ThreadCache.cpp》
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	//从自由链表中取出批量个对象
	list.PopRange(start, end, list.MaxSize());
	//将批量的内存对象返还到对应的span中
	CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}

基于以上需求,我们应该对自由链表FreeList类进行修改,新增一个Size函数用来获取自由链表中的个数,新增一个PopRange函数用于从自由链表中取出指定个数的内存对象,并且新增一个_size变量记录自由链表中内存对象的个数,任何有对自由链表内存对象个数修改的地方都应该更新_size变量

《Common.h》
//管理切分好的小对象的自由链表
class FreeList
{
public:
	//将释放的对象头插到自由链表
	void Push(void* obj)
	{
		assert(obj);

		//头插
		NextObj(obj) = _freeList;
		_freeList = obj;
		_size++;
	}
	//从自由链表头部获取一个对象
	void* Pop()
	{
		assert(_freeList);

		//头删
		void* obj = _freeList;
		_freeList = NextObj(_freeList);
		_size--;

		return obj;
	}
	//插入一段范围的对象到自由链表
	void PushRange(void* start, void* end, size_t n)
	{
		assert(start);
		assert(end);

		//头插
		NextObj(end) = _freeList;
		_freeList = start;
		_size += n;
	}
	//从自由链表获取一段范围的对象
	void PopRange(void*& start, void*& end, size_t n)
	{
		assert(n <= _size);

		//头删
		start = _freeList;
		end = start;
		for (size_t i = 0; i < n - 1;i++)
		{
			end = NextObj(end);
		}
		_freeList = NextObj(end); //自由链表指向end的下一个对象
		NextObj(end) = nullptr; //取出的一段链表的表尾置空
		_size -= n;
	}
	bool Empty()
	{
		return _freeList == nullptr;
	}
	size_t& MaxSize()
	{
		return _maxSize;
	}
	size_t Size()
	{
		return _size;
	}
private:
	void* _freeList = nullptr; //自由链表
	size_t _maxSize = 1;
	size_t _size = 0;
};

二、CentralCache回收内存

1、当threadcache将自由链表中的内存对象返还给centralcache的span的时候会存在一个问题,这些对象应该返还给哪一个span,在centralcache中每个哈希桶当中都可能不止一个span,因此我们需要知道每个对象对应的是哪一个span
2、我们知道一个span可以切分为多个小内存对象,对内存对象的地址➗一页的大小(假设为8K),可以得到该内存对象所在的页号,一个span结构包含页号、页数等等成员变量,由此我们可以建立页号与span的对应关系,这样,已知内存对象的地址就可以知道页号,进一步就可以得知该内存对象是属于哪一个span了

比如从2000页到2001页之间的任意地址(一个个小内存对象的地址)➗8K都是2000,因为其它地方不够整除就舍弃了

《UnitTest.cpp》
void TestAddressShift()
{
	PAGE_ID id1 = 2000;      //页号1
	PAGE_ID id2 = 2001;     //页号2
	char* p1 = (char*)(id1 << PAGE_SHIFT);   //获取内存块的起始地址
	char* p2 = (char*)(id2 << PAGE_SHIFT);
	while (p1 < p2)
	{
		cout << (void*)p1 << ":" << ((PAGE_ID)p1 >> PAGE_SHIFT) << endl;   //获取每次地址对应对应的页号
		p1 += 8;  
	}
}

在这里插入图片描述

在这里插入图片描述

2.1 建立映射

知道一个内存块的地址,就能知道页号,建一个map<页号与span指针的映射>就能找到span

《PageCache.h》
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	//获取一个K页的span
	Span* NewSpan(size_t K);

	//获取从对象到span的映射
	Span* MapObjectToSpan(void* obj);

	//释放空闲span回到pagecache,并合并相邻的span
	void ReleaseSpanToPageCache(Span* span);

	std::mutex _pageMutex;
private:
	SpanList _spanLists[NPAGES];
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;
private:
	PageCache()
	{}

	
	//防拷贝
	PageCache(const PageCache&) = delete;
	static PageCache _sInst;
};

最初span从哪里来呢?当最初申请内存对象的时候,会一层层往下找,直到找到pagecache,pagecache从堆中申请出一个128页的span,并按照申请的需求切分给centralcache,由此我们应该从pagecache申请出来span的时候建立映射

《PageCache.cpp》
//在pagecache中获取一个n页的span
// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	// 大于128 page的直接向堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		//Span* span = new Span;
		Span* span = _spanPool.New();

		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;

		_idSpanMap[span->_pageId] = span;	//建立映射关系
		return span;
	}

	//检查pagecache第K个桶是否有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; ++i)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	//查看第K个桶的后面的桶是否有span(K+1:跳过当前没有span的桶)
	for (size_t i = k + 1; i < NPAGES; ++i)
	{
		if (!_spanLists[i].Empty())
		{
			//切分span
			Span* nSpan = _spanLists[i].PopFront();
			//Span* kSpan = new Span;
			Span* kSpan = _spanPool.New();

			// 在nSpan的头部切一个k页下来
			// k页span返回
			// nSpan再挂到对应映射的位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//将切分剩下的页缓存重新挂起来
			_spanLists[nSpan->_n].PushFront(nSpan);

			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PAGE_ID i = 0; i < kSpan->_n; ++i)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	//其余桶为空,此时向(堆)系统申请一个128Page的内存块
	//Span* bigSpan = new Span;
	Span* bigSpan = _spanPool.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;
	//在pagecache对应的桶中插入刚申请的内存span
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	//复用自己,重新进行切分
	return NewSpan(k);
}

由此我们就可以根据内存对象的地址转换成页号,在unordered_map当中找到所对应的span了
如果存在页号所对应span的映射,返回span,否则直接结束程序,因为在获取一个span的时候,我们就已经对其进行映射,如果不存在,只能说明代码逻辑有问题

《PageCache.cpp》
Span* PageCache::MapObjectToSpan(void* obj)
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else 
	{
		assert(false);
		return nullptr;
	}
}

遍历自由链表上的内存对象,并获取其与span的映射关系,然后再头插到对应的span管理的自由链表上,每次归还一个内存对象,就对该内存对象所属的span结构中的_usecount进行–,如果减到0,说明这个span分配出去的内存对象全部归还回来了,此时就可以将此span进一步归还给下一层page cache

《CentralCache.cpp》
void CentralCache::ReleaseListToSpans(void*& start, size_t size)
{ 
	size_t index = AlignmentRules:: Index(size);
	//将threadcache上的自由链表上的每一个内存对象归还给对应的span中
	_spanLists[index]._mtx.lock();
	while (start)
	{
		//保存下一个
		void* next = FreeList::NextObj(start);
		//获取span与自由链表中内存对象的映射关系
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//头插
		FreeList::NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;		//更新分配给threadcache的计数
		//全部内存对象已经归还,此时可以将span归还给下一层pagecache中
		if (span->_useCount == 0)
		{
			//将此span从原有的链表中解除
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_prev = nullptr;
			span->_next = nullptr;
			//解桶锁,因为此span要返还给下一层,已经不用了
			_spanLists[index]._mtx.unlock();
			PageCache::GetInstance()->_pageMutex.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMutex.unlock();
			_spanLists[index]._mtx.lock();
		}

		//进行下一个内存对象的归还
		start = next;
	}
	_spanLists[index]._mtx.unlock();
}

值得一提的是,当_usecount为0的时候,此时就需要把某个span归还给下一层,首先应该把该span在centralcache中
对应某个桶的span双链表中移除,然后将该span的自由链表置空,因为在page cache中的span是不需要切分成一个个小的内存对象的,另外该span的前后指针也是需要置空的,因为要重新插入pagecache中,需要把该span在centralcache中的前后关系清除。

三、PageCache回收内存

1、当central cache中的某个span的_useCount减到0了,此时central
cache就需要把此span返还给下一层page cache。
2、为了缓解内存碎片问题,pagecache还需要将此span与其余空闲的span进行合并,合并分为向前与向后合并

向前合并:
向前合并的时候,需要判断第x-1页的span是否空闲,如果空闲就进行合并,合并后还需要继续向前进行合并,直到不能合并为止
在这里插入图片描述
向后合并:
向后合并的时候,需要判断第x+_n页的span是否空闲,如果空闲就进行合并,合并后还需要继续向后进行合并,直到不能合并为止
在这里插入图片描述

1、在向前合并的时候,我们会用当前页号-1得到上一个span的末尾页号,在centralcache回收内存中,我们对每一个从pagecache申请的span与其页号一一映射,为的就是让threadcache中每一个内存对象能通过内存对象的起始地址算出页号找到对应的span,好归还到每一个centralcache的span当中,而在pagecache回收内存中,我们需要建立一个span的首尾页号与其span的映射,方便向前与向后合并
2、因此当我们申请k页的span时,如果是将n页的span切成了一个k页的span和一个n-k页的span,我们除了需要建立k页span中每个页与该span之间的映射关系之外,还需要建立剩下的n-k页的span与其首尾页之间的映射关系。
3、并且我们需要一个_isUse成员变量来判断前后span是否正在被使用

增加_isUse变量

//管理以页为单位的大块内存
《Common.h》
struct Span
{
	PAGE_ID _pageId = 0;        //大块内存起始页的页号
	size_t _n = 0;              //页的数量

	Span* _next = nullptr;      //双链表结构
	Span* _prev = nullptr;

	size_t _useCount = 0;       //切好的小块内存,被分配给thread cache的计数
	void* _freeList = nullptr;  //切好的小块内存的自由链表

	bool _isUse = false;        //是否在被使用
};

对一个span的首位页号进行映射

//在pagecache中获取一个n页的span
《PageCache.cpp》
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	// 大于128 page的直接向堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		//Span* span = new Span;
		Span* span = _spanPool.New();

		span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;

		_idSpanMap[span->_pageId] = span;	//建立映射关系
		return span;
	}

	//检查pagecache第K个桶是否有span
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; ++i)
		{
			_idSpanMap[kSpan->_pageId + i] = kSpan;
		}

		return kSpan;
	}

	//查看第K个桶的后面的桶是否有span(K+1:跳过当前没有span的桶)
	for (size_t i = k + 1; i < NPAGES; ++i)
	{
		if (!_spanLists[i].Empty())
		{
			//切分span
			Span* nSpan = _spanLists[i].PopFront();
			//Span* kSpan = new Span;
			Span* kSpan = _spanPool.New();

			// 在nSpan的头部切一个k页下来
			// k页span返回
			// nSpan再挂到对应映射的位置
			kSpan->_pageId = nSpan->_pageId;
			kSpan->_n = k;

			nSpan->_pageId += k;
			nSpan->_n -= k;
			//将切分剩下的页缓存重新挂起来
			_spanLists[nSpan->_n].PushFront(nSpan);
			//存储Nspan的首位页号与Nspan映射,方便page cache回收内存时进行合并查找
			_idSpanMap[nSpan->_pageId] = nSpan;//首  
			_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;//尾


			// 建立id和span的映射,方便central cache回收小块内存时,查找对应的span
			for (PAGE_ID i = 0; i < kSpan->_n; ++i)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}
	}

	//其余桶为空,此时向(堆)系统申请一个128Page的内存块
	//Span* bigSpan = new Span;
	Span* bigSpan = _spanPool.New();
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;
	//在pagecache对应的桶中插入刚申请的内存span
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	//复用自己,重新进行切分
	return NewSpan(k);
}

在向前或向后进行合并的过程中:

1、如果没有通过页号获取到其对应的span,说明对应到该页的内存块还未申请,此时需要停止合并。
2、如果通过页号获取到了其对应的span,但该span处于被使用的状态,那我们也必须停止合并。
3、如果合并后大于128页则不能进行本次合并,因为page cache无法对大于128页的span进行管理。

向前向后合并空闲span

《PageCache.cpp》
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//如果申请的内存是从堆来的
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		//delete span;
		_spanPool.Delete(span);
		return;
	}
	while (1)
	{
		//对span前的页,尝试进行合并,缓解内存碎片问题
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		//查找前一个span是否存在,若查找到尾,则不存在
		if (ret == _idSpanMap.end())
		{
			break;
		}
		Span* prevSpan = ret->second;
		//超出所合并的最大页数
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//若span正在使用,停止合并
		if (prevSpan->_isUse == true)
		{
			break;
		}
		//合并前后页
		span->_n += ret->second->_n;
		span->_pageId = prevId;
		//将以被合并的span从链表中解除
		_spanLists[prevSpan->_n].Erase(prevSpan);
		//delete prevSpan;
		_spanPool.Delete(prevSpan);
	}

	while(1)
	{ 
		//对span后的页,尝试进行合并,缓解内存碎片问题
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		//查找前一个span是否存在,若查找到尾,则不存在
		if (ret == _idSpanMap.end())
		{
			break;
		}
		Span* nextSpan = ret->second;
		//超出所合并的最大页数
		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		//若span正在使用,停止合并
		if (nextSpan->_isUse == true)
		{
			break;
		}

		//合并前后页
		span->_n += ret->second->_n;
		//将以被合并的span从链表中解除
		_spanLists[nextSpan->_n].Erase(nextSpan);
		//delete nextSpan;
		_spanPool.Delete(nextSpan);
	}
	//插入所合并好的span
	_spanLists->PushFront(span);
	//建立该span与其首位页的映射
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId += span->_n - 1] = span;
	//将刚合并好的span设置成未使用的状态
	span->_isUse = false;
}

小结

今日的项目分享就到这里啦,做完一部分项目一定要好好总结呀,不能贪快,如果本文存在疏漏或错误的地方,还请您能够指出

posted @ 2024-04-09 12:22  Fan_558  阅读(4)  评论(0编辑  收藏  举报  来源