【tcmalloc】(六)释放流程

一.整体框架

tcmalloc的源码考虑了内存的大小和链表的长度。这里实现的是简单的版本之考虑了链表的长度  。当释放内存块后会挂到线程缓冲区对应的自由链表上,当检测到当前自由链表的长度大于我们一次批量申请的大小之后就会将当前自由链表的一部分(一个批量)还给centralcacahe。不是全拿走,有可能有剩余个其他先线程使用。centralcache拿到以后要继续向pagecache归还

二.线程归还(还自由链表)

计算出来那个桶节点的自由链表后,先把当前块的内存的还给自由链表。在判断当前链表的长度是否需要归还给centralcache。

//线程缓冲区归还内存 
void* ThreadCache::Deallocate(void* obj, size_t size)		//需要知道内存大小好计算在哪个桶位置的自由链表上插入
{
	assert(obj);
	assert(size <= MAX_BYTES);

	size_t index = SizeRules::Index(size);  //寻找还回来的内存块可以插入桶节点的位置
	_freelists[index].Push(obj);  //插入得到的内存块

	//当换回来的内存块超过一定值的时候,统一还给span内存块(central catche)
	//这里用一次批量申请的上限作为阀值(慢启动算法决定的那个值)
	if (_freelists[index].MemSize() >= _freelists[index].MaxSize())		//不一定memsize从0开始,_freelist挂的是没使用的内存若申请多了没使用memsize就不是0
	{
		//合并自由链表的小内存块并统一返还给centralcache
		ListTooLong(_freelists[index], size);
	}
	return obj;
}

释放给centralcache的中间逻辑。需要归还的话再把自由链表切下来一块还给span

void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
	//先找到自由链表中的要返还的内存块
	void* start = nullptr;
	void* end = nullptr;

	list.PopRange(start, end, list.MaxSize());	//每次直接返回每批次的最大值,有可能会留下一部分

	CentralCache::GetCent()->ReleaseListToSpans(start, size);
}

三.中央内存区归还(还span块)

首先根据线程缓冲区要释放的每个节点大小计算出这些节点应该来自于多大的span,从而确定桶下标。同时因为我们可以通过物理地址计算出页号,只要在当前自由链表下找到对应的页号,这样就能找到要归还的span块所在的自由链表的位置。(注意,要归还的自由链表节点有可能不连续且来自多个span。)

但每次遍历整个链表效率过于低下n^2的算法,可以采用哈希表直接映射。构建页号和页块的映射关系。这样我们通过物理地址计算出页号后能直接找到页块位置。这套逻辑由于pagecache也要使用所以写在pagecache模块里

在通过获取到span块后,将当前小内存块还给span然后继续归还,指导整段自由链表全都被归还给span。

这样挂回span块的自由链表其实是乱序的,但并不影响使用,因为所有span块里已经记录了当前区间下自由链表的起始页号。所以链表可以直接置空。

通过对span块内的usecount(使用自由链表节点数)的监控在当前span块全部被归还的时候继续将span块归还给pagecache,同时注意锁切换的问题

void CentralCache::ReleaseListToSpans(void* start, size_t size)//注意点由于释放内存自由链表的顺序不固定,所以可能来自于不同的span
{
	//先计算出释放的内存大小对应的桶节点
	size_t index = SizeRules::Index(size);
	//由于访问中央内存区,需要加锁
	_spanList[index].Getmtx().lock();
	//将所有节点归还给对应的span
	while (start)
	{
		//找到当前内存对应的span
		Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
		//自由链表的内存块依次头插进span
		void* next = NextObj(start);	//保存	

		NextObj(start) = span->_freeList;//头插
		span->_freeList = start;
	
		span->_useCount--;

		if (span->_useCount == 0)				// 说明span的切分出去的所有小块内存都回来了
		{										// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
			_spanList[index].Erase(span);		//在当前桶节点里删掉这个span块还给page

			span->_freeList = nullptr;
			span->_prev = nullptr;
			span->_next = nullptr;

			//继续往下走pagecache的合并逻辑
			//和申请一样这里不用再用桶锁了,转为使用page的整体锁
			_spanList[index].Getmtx().unlock();

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

			//出page后再把central的锁加回来
			_spanList[index].Getmtx().lock();
		}
		start = next;
	}
	_spanList[index].Getmtx().unlock();
}


四.pagecache释放逻辑(合并内存块+释放真正的内存)

上文说过,因为我们需要页号和页块的映射关系。所以在申请的时候也需要在申请k页span的时候增加上映射关系。

通过物理地址获得span块(哈希表结构)

//通过释放内存块的物理地址计算出来自哪个内存块
Span* PageCache::MapObjectToSpan(void* obj)  //传入当前内存块的起始位置
{
	PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);	//先通过物理地址和页号之间的关系计算出页号。一个页号可以对应多块物理地址。
	
	std::unique_lock<std::mutex> lock(_pagemtx);
	auto ret = _idSpanMap.find(id);		//哈希映射通过id找到span
	if (ret != _idSpanMap.end())		//找到了
	{
		return ret->second;
	}
	else							//逻辑正确的情况下不应该找不到
	{
		assert(false);
		return nullptr;
	}

}

pagecache拿到还回来的span内存块的时候要进行内存合并,可以通过上面的哈希映射关系判断前后页号的span块是否被使用,在pagecache的都可以合并,在centralcache的不能被合并。重点,不能用span下的 usecount来判断当前span块能否被合并,有一种间隙情况, k页的span刚被申请出来还没有被切分,此时usecout仍让是0,若此时被后一个span合并就会产生冲突,需要新增一个变量记录是否被使用。

所有切分出去和没切分出去的span块都需要通过页号建立映射关系,但注意,没切分出去的span块只需要在span块的开头和结尾记录映射关系即可,因为合并的时候只能从前面或者后面搜索映射关系。当他要被使用的时候才会被构建更为详细的映射关系。

拿到span块后不断合并,这是形成一个更大的span块,直到无法合并的时候。最后只有当合并的内存块等于128的时候才会真正的进入内存释放阶段。同时span块的合并就是页号的改变,同时删掉被合并页块的属性结构体,此时不再关注底层的自由链表,自由链表在centralcache阶段就已经被释放了。

//将释放回来的span快整合并划给pagecache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//大于128页的直接还给堆
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
		Memreq::SystemFree(ptr);
		_spanPool.Delete(span);

		return;
	}
	else
	{
		//不断让页前后合并以解决内存外碎片问题
		while (1)
		{
			//向前合并
			PAGE_ID prev_id = span->_pageid - 1;	//因为要合并连续页号我们首先要判断当前位置的前一个页号是否存在(有可能并未申请过)
			auto ret = (Span*)_idSpanMap.get(prev_id);
			//前一个页号不存在无法合并
			if (ret == nullptr)
			{
				break;
			}

			//走到这里说明前一个页号存在继续判断是否正在使用
			Span* prevSpan = ret;	//拿到这个页号对应的span

			if (prevSpan->_isUse == true)
			{
				break;
			}
			//合并后span的大小要小于最大的span值
			if (prevSpan->_n + span->_n > NPAGES - 1)
			{
				break;
			}

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

			//合并后要把spanlist上的被合并的span删除掉
			_spanlists[prevSpan->_n].Erase(prevSpan);

			_spanPool.Delete(prevSpan);
		}

		while (1)
		{
			//向后合并
			PAGE_ID next_id = span->_pageid + span->_n;	//因为要合并连续页号我们首先要判断当前位置的前一个页号是否存在(有可能并未申请过)
			auto ret = (Span*)_idSpanMap.get(next_id);
			//前一个页号不存在无法合并
			if (ret == nullptr)
			{
				break;
			}

			//走到这里说明前一个页号存在继续判断是否正在使用
			Span* nextSpan = ret;	//拿到这个页号对应的span

			if (nextSpan->_isUse == true)
			{
				break;
			}
			//合并后span的大小要小于最大的span值
			if (nextSpan->_n + span->_n > NPAGES - 1)
			{
				break;
			}


			span->_n += nextSpan->_n;

			//合并后要把spanlist上的被合并的span删除掉
			_spanlists[nextSpan->_n].Erase(nextSpan);

			_spanPool.Delete(nextSpan);

		}

		//span已经合并完成重新添加进spanlist
		_spanlists[span->_n].PushFront(span);
		//并在pageid和span之间的映射关系上加上自己,等同于申请span时没使用的span
		_idSpanMap.set(span->_pageid,span);
		_idSpanMap.set(span->_pageid + span->_n - 1, span);
		//当前span合并完毕可以被其他块合并

		span->_isUse = false;
	}
	
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TCMalloc是一个由Google开发的内存管理器,被广泛用于大规模的分布式系统中。它为多线程环境下的内存分和释放提供了高效的性能。 TCMalloc的实现主要包含以下几个关键特性: 1. Thread-Caching TCMalloc使用了线程本地缓存(Thread-Caching)的机制,每个线程都维护了一个本地缓存,用于快速分配释放内存。这样可以避免不同线程之间频繁访问共享数据结构的开销,提高了内存分配释放的性能。 2. Central Cache TCMalloc还维护了一个全局的中央缓存(Central Cache),用于存储较大的内存块。当本地缓存不足以满足内存需求时,线程会向中央缓存申请内存。 3. Page Heap TCMalloc使用了Page Heap来管理内存。Page Heap将内存按页进行划分,并使用位图来跟踪每个页的使用情况。这样可以提高内存分配释放的效率,并减少内存碎片化。 4. Size Class TCMalloc将需要分配内存按照大小进行分类,每个大小类都有一个对应的内存池。这样可以提高内存分配释放的效率,并减少内存碎片化。 5. 高效的内存分配释放算法 TCMalloc使用了一些高效的算法来管理内存,如位图、堆栈缓存等。这些算法能够快速地分配释放内存,并且减少内存碎片化。 总的来说,TCMalloc通过使用线程本地缓存、中央缓存、Page Heap以及高效的内存分配释放算法,实现了高性能的内存管理。它在Google的大规模分布式系统中得到了广泛应用,并取得了很好的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值