《C++实战项目-高并发内存池》6.内存释放流程

💡Yupureki:个人主页

✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》


🌸Yupureki🌸的简介:


目录

[1. 准备工作](#1. 准备工作)

[2. ThreadCache内存回收与释放](#2. ThreadCache内存回收与释放)

[3. CentralCache内存回收与释放](#3. CentralCache内存回收与释放)

[4. PageCache内存回收与释放](#4. PageCache内存回收与释放)


1. 准备工作

当ThreadCache把内存块还给CentralCache时,这些内存块挂在哪里?我们知道这些内存块之前是在一个Span下的,而也应该理所应当还给那个Span

因此我们必须用哈希表记录一个内存块对应的Span。由于每个Span也都是PageCache给CentralCache的,对应关系也应该由PageCache知道,因此我们得在PageCache内新增一个内存块地址查找Span的哈希表

cpp 复制代码
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

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

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

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

	std::mutex _pageMtx;
private:
	SpanList _spanLists[NPAGES];

	std::unordered_map<PAGE_ID, Span*> _idSpanMap;//哈希表:页ID到Span的映射关系

	PageCache()
	{}
	PageCache(const PageCache&) = delete;


	static PageCache _sInst;
};

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

何时更新_idSpanMap?在PageCache中,Span内的内存块是一个连续的大块的内存块,也没有被使用,还不需要被记录。直到CentralCache申请拿走后,要进行切分,此时一块块的内存就需要存储对应的Span了。此环节对应PageCache的NewSpan函数中。因此该函数需要更新

cpp 复制代码
// 获取一个K页的span
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);

	// 先检查第k个桶里面有没有span
	if (!_spanLists[k].Empty())
	{
		return _spanLists->PopFront();
	}

	// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
	for (size_t i = k+1; i < NPAGES; ++i)
	{
		if (!_spanLists[i].Empty())
		{
			Span* nSpan = _spanLists[i].PopFront();
			Span* kSpan = new Span;

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

	// 走到这个位置就说明后面没有大页的span了
	// 这时就去找堆要一个128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1;

	_spanLists[bigSpan->_n].PushFront(bigSpan);

	return NewSpan(k);
}

2. ThreadCache内存回收与释放

当ThreadCache把内存收回后,会把内存块串在对应的哈希桶内

cpp 复制代码
void ThreadCache::Deallocate(void* ptr, size_t size)
{
	assert(ptr);
	assert(size <= MAX_BYTES);

	// 找对映射的自由链表桶,对象插入进入
	size_t index = SizeClass::Index(size);
	_freeLists[index].Push(ptr);

	// 当链表长度大于一次批量申请的内存时就开始还一段list给central cache
	if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
	{
		ListTooLong(_freeLists[index], size);
	}
}

当哈希桶内的内存块过多时,就应该还给CentralCache

cpp 复制代码
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	void* start = nullptr;
	void* end = nullptr;
	list.PopRange(start, end, list.MaxSize());//把过长的内存块拿出

	CentralCache::GetInstance()->ReleaseListToSpans(start, size);//还给CentralCache
}

3. CentralCache内存回收与释放

当CentralCache收回ThreadCache还回来的内存块:

  1. 通过PageCache内的哈希表查找内存块对应的Span
  2. 串在对应的Span下
  3. 如果该Span没有任何内存块被ThreadCache拿走使用去了,直接还给PageCache
cpp 复制代码
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
	size_t index = SizeClass::Index(size);
	_spanLists[index]._mtx.lock();
	while (start)
	{
		void* next = NextObj(start);

		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		NextObj(start) = span->_freeList;
		span->_freeList = start;
		span->_useCount--;

		// 说明span的切分出去的所有小块内存都回来了
		// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
		if (span->_useCount == 0)
		{
			_spanLists[index].Erase(span);
			span->_freeList = nullptr;
			span->_next = nullptr;
			span->_prev = nullptr;

			// 释放span给page cache时,使用page cache的锁就可以了
			// 这时把桶锁解掉
			_spanLists[index]._mtx.unlock();

			PageCache::GetInstance()->_pageMtx.lock();
			PageCache::GetInstance()->ReleaseSpanToPageCache(span);
			PageCache::GetInstance()->_pageMtx.unlock();

			_spanLists[index]._mtx.lock();
		}

		start = next;
	}

	_spanLists[index]._mtx.unlock();
}

4. PageCache内存回收与释放

当PageCache收回CentralCache还回来的内存块:

  1. 将该内存块向前和向后合并,一直到无法合并为止
  2. 合并后,串在对应的哈希桶内
  3. 合并后,对应的哈希映射关系需要清除
cpp 复制代码
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 对span前后的页,尝试进行合并,缓解内存碎片问题
	while (1)
	{
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);
		// 前面的页号没有,不合并了
		if (ret == _idSpanMap.end())
		{
			break;
		}

		// 前面相邻页的span在使用,不合并了
		Span* prevSpan = ret->second;
		if (prevSpan->_isUse == true)
		{
			break;
		}

		// 合并出超过128页的span没办法管理,不合并了
		if (prevSpan->_n + span->_n > NPAGES-1)
		{
			break;
		}

		span->_pageId = prevSpan->_pageId;
		span->_n += prevSpan->_n;

		_spanLists[prevSpan->_n].Erase(prevSpan);
		delete prevSpan;
	}

	// 向后合并
	while (1)
	{
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES-1)
		{
			break;
		}

		span->_n += nextSpan->_n;

		_spanLists[nextSpan->_n].Erase(nextSpan);
		delete nextSpan;
	}

	_spanLists[span->_n].PushFront(span);
	span->_isUse = false;
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId+span->_n-1] = span;
}
相关推荐
badhope2 小时前
一命速通蓝桥杯全攻略
开发语言·前端·人工智能·python·职场和发展·蓝桥杯·github
charlie1145141912 小时前
嵌入式现代C++开发——三路比较运算符
开发语言·c++·学习·算法·嵌入式·编程指南
2401_900151542 小时前
C++编译期正则表达式
开发语言·c++·算法
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践1 kicad pcb 格式讲解
算法·agent·pcb·eda·routing
仙俊红2 小时前
LeetCode493周赛T3,前后缀分解
数据结构·算法·leetcode
geovindu2 小时前
python: 初养龙虾微信纯文字自动回复using workBuddy
开发语言·python·ocr·腾讯云ai代码助手
_日拱一卒2 小时前
LeetCode(力扣):二叉树的前序遍历
java·数据结构·算法·leetcode
倾心琴心2 小时前
【agent辅助pcb routing coding学习】实践5 kicad类按类别理解
算法·agent·pcb·eda·routing