【高并发内存池】第七弹---释放内存过程联调测试与大于256KB内存申请全攻略

发布于:2025-04-01 ⋅ 阅读:(27) ⋅ 点赞:(0)

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】【项目详解】

目录

1、释放内存过程联调测试

2、大于256KB的大块内存申请问题

2.1、申请内存过程

2.2、释放内存过程

2.3、测试


1、释放内存过程联调测试

之前我们在测试申请流程时,让单个线程进行了五次内存申请,但是申请五次不能够直接把内存释放完,因此我们此处申请三次内存,看看这其中的释放流程是如何进行的

void TestConcurrentAlloc3()
{
	void* p1 = ConcurrentAlloc(6);
	void* p2 = ConcurrentAlloc(8);
	void* p3 = ConcurrentAlloc(1);
	void* p4 = ConcurrentAlloc(7);
	void* p5 = ConcurrentAlloc(8);

	cout << p1 << endl;
	cout << p2 << endl;
	cout << p3 << endl;
	cout << p4 << endl;
	cout << p5 << endl;

	ConcurrentFree(p1, 6);
	ConcurrentFree(p2, 8);
	ConcurrentFree(p3, 1);
	ConcurrentFree(p4, 7);
	ConcurrentFree(p5, 8);
}

 1、这三次申请和释放的对象大小进行对齐后都是8字节,因此对应操作的就是thread cache和central cache的第0号桶,以及page cache的第1号桶

2、由于第三次对象申请时,刚好将thread cache第0号桶当中仅剩的一个对象拿走了,因此在三次对象申请后thread cache的第0号桶当中是没有对象的

3、通过监视窗口可以看到,此时thread cache第0号桶中自由链表的_maxSize已经慢增长到了3,而当我们释放完第一个对象后,该自由链表当中对象的个数只有一个,因此不会将该自由链表当中的对象进一步还给central cache

当第二个对象释放给thread cache的第0号桶后,该桶对应自由链表当中对象的个数变成了2也是不会进行ListTooLong操作的

直到第三个对象释放给thread cache的第0号桶时,此时该自由链表的_size的值变为3,与_maxSize的值相等,现在thread cache就需要将对象给central cache了

thread cache先是将第0号桶当中的对象弹出MaxSize个,在这里实际上就是全部弹出,此时该自由链表_size的值变为0,然后继续调用central cache当中的ReleaseListToSpans函数,将这三个对象还给central cache当中对应的span。 

在进入central cache的第0号桶还对象之前先把第0号桶对应的桶锁加上,然后通过查page cache中的映射表找到其对应的span,最后将这个对象头插到该span的自由链表中,并将该span的_useCount进行--。当第一个对象还给其对应的span时,可以看到该span的_useCount减到了2

由于我们只进行了三次对象申请,并且这些对象大小对齐后大小都是8字节,因此我们申请的这三个对象实际都是同一个span切分出来的。当我们将这三个对象都还给这个span时,该span的_useCount就减为了0。 

现在central cache就需要将这个span进一步还给page cache,而在将该span交给page cache之前,会将该span的自由链表以及前后指针都置空。并且在进入page cache之前会先将central cache第0号桶的桶锁解掉,然后再加上page cache的大锁,之后才能进入page cache进行相关操作。

由于这个一页的span是从128页的span的头部切下来的,在向前合并时由于前面的页还未向系统申请,因此在查映射关系时是无法找到的,此时直接停止了向前合并。 

同理, 在向后合并时由于后面的页还未向系统申请,因此在查映射关系时是无法找到的,此时直接停止了向后合并

将这个1页的span插入到第1号桶,然后建立该span与其首尾页的映射,便于下次被用于合并,最后再将该span的状态设置为未被使用的状态即可。

当从page cache回来后,除了将page cache的大锁解掉,还需要立刻加上central cache中对应的桶锁,然后继续将对象还给central cache中的span,但此时实际上是还完了,因此再将central cache的桶锁解掉就行了。 

至此我们便完成了这三个对象的申请和释放流程。

2、大于256KB的大块内存申请问题

2.1、申请内存过程

前面说到,每个线程的thread cache是用于申请小于等于256KB的内存的,而对于大于256KB的内存,我们可以考虑直接向page cache申请,但page cache中最大的页也就只有128页,因此如果是大于128页的内存申请,就只能直接向堆申请了。

申请内存的大小 申请方式
X<=256KB(32页) 向thread cache申请
32页 < X <= 128页 向page cache申请
x >= 128页 向堆申请

注意:当申请的内存大于256KB时,虽然不是从thread cache进行获取,但在分配内存时也是需要进行向上对齐的,对于大于256KB的内存我们可以直接按页进行对齐。

我们之前实现RoundUp函数时,对传入字节数大于256KB的情况直接做了断言处理,因此这里需要对RoundUp函数稍作修改

static inline size_t RoundUp(size_t size)
{
	if (size <= 128) // 1-128 按照8字节对齐
	{
		return _RoundUp(size, 8);
	}
	else if (size <= 1024)
	{
		return _RoundUp(size, 16);
	}
	else if (size <= 8 * 1024)
	{
		return _RoundUp(size, 128);
	}
	else if (size <= 64 * 1024)
	{
		return _RoundUp(size, 1024);
	}
	else if (size <= 256 * 1024)
	{
		return _RoundUp(size, 8 * 1024);
	}
	else
	{
		// 大于256KB按页对齐 
		return _RoundUp(size, 1 << PAGE_SHIFT);
	}
}

 对于之前的申请对象的逻辑就需要进行修改了,当申请对象的大小大于256KB时,就不用向thread cache申请了,这时先计算出按页对齐后实际需要申请的页数,然后通过调用NewSpan申请指定页数的span即可。

// 申请内存
static void* ConcurrentAlloc(size_t size)
{
	if (size > MAX_BYTES)
	{
		// 计算出对齐后需要申请的页数
		size_t alignSize = SizeClass::RoundUp(size);
		size_t kPage = alignSize >> PAGE_SHIFT;

		// 向page cache申请kPage页的span
		PageCache::GetInstance()->_pageMtx.lock(); // 加大锁
		Span* span = PageCache::GetInstance()->NewSpan(kPage);
		PageCache::GetInstance()->_pageMtx.unlock(); // 解除大锁

		void* ptr = (void*)(span->_pageID << PAGE_SHIFT);
		return ptr;
	}
	else
	{
		// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
		if (pTLSThreadCache == nullptr)
		{
			pTLSThreadCache = new ThreadCache;
		}

		cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;

		return pTLSThreadCache->Allocate(size);
	}
}

也就是说,申请大于256KB的内存时,会直接调用page cache当中的NewSpan函数进行申请,因此这里我们需要再对NewSpan函数进行改造,当需要申请的内存页数大于128页时,就直接向堆申请对应页数的内存块。而如果申请的内存页数是小于128页的,那就在page cache中进行申请,因此当申请大于256KB的内存调用NewSpan函数时也是需要加锁的,因为我们可能是在page cache中进行申请的。

// 获取一个K页的span(加映射版本)
Span* PageCache::NewSpan(size_t k)
{
	// assert(k > 0 && k < NPAGES); // 保证在桶的范围内
	assert(k > 0);
	// 大于128页直接找堆申请
	if (k > NPAGES - 1)
	{
		void* ptr = SystemAlloc(k);
		Span* span = new Span;
		span->_pageID = (PAGE_ID)ptr >> PAGE_SHIFT;
		span->_n = k;
		// 建立页号与span之间的映射
		_idSpanMap[span->_pageID] = span;
		return span;
	}

	// 1.检查第k个桶里面有没有Span,有则返回
	if (!_spanLists[k].Empty())
	{
		Span* kSpan = _spanLists[k].PopFront();

		// 建立页号与span的映射,方便central cache回收小块内存查找对应的span
		for (PAGE_ID i = 0; i < kSpan->_n; i++)
		{
			_idSpanMap[kSpan->_pageID + i] = kSpan;
		}
		return kSpan;
	}
	// 2.检查一下后面的桶里面有没有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页下来
			kSpan->_pageID = nSpan->_pageID;
			kSpan->_n = k;

			// 更新nSpan位置
			nSpan->_pageID += k;
			nSpan->_n -= k;

			// 将剩下的挂到对应映射的位置
			_spanLists[nSpan->_n].PushFront(nSpan);

			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageID + i] = kSpan;
			}

			return kSpan;
		}
	}
	// 3.没有大页的span,找堆申请128页的span
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1); // 申请128页内存
	bigSpan->_pageID = (PAGE_ID)ptr >> PAGE_SHIFT;
	bigSpan->_n = NPAGES - 1; // 页数

	// 将128页span挂接到128号桶上
	_spanLists[bigSpan->_n].PushFront(bigSpan);

	// 递归调用自己(避免代码重复)
	return NewSpan(k);
}

2.2、释放内存过程

当释放对象时,我们需要判断释放对象的大小:

因此当释放对象时,我们需要先找到该对象对应的span,但是在释放对象时我们只知道该对象的起始地址。这也就是我们在申请大于256KB的内存时,也要给申请到的内存建立span结构,并建立起始页号与该span之间的映射关系的原因。此时我们就可以通过释放对象的起始地址计算出起始页号,进而通过页号找到该对象对应的span

// 释放内存
static void ConcurrentFree(void* ptr, size_t size/*暂时*/)
{
	// 大于256KB的内存释放
	if (size > MAX_BYTES)
	{
		Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);

		PageCache::GetInstance()->_pageMtx.lock();
		PageCache::GetInstance()->ReleaseSpanToPageCache(span);
		PageCache::GetInstance()->_pageMtx.unlock();
	}
	else
	{
		assert(pTLSThreadCache);

		pTLSThreadCache->Deallocate(ptr, size);
	}
}

因此page cache在回收span时也需要进行判断,如果该span的大小是小于等于128页的,那么直接还给page cache就行了,page cache会尝试对其进行合并。而如果该span的大小是大于128页的,那么说明该span是直接向堆申请的,我们直接将这块内存释放给堆,然后将这个span结构进行delete就行了。

// 释放空闲的span回到PageCache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	// 大于128页直接释放给堆
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageID << PAGE_SHIFT);
		SystemFree(ptr);
		delete span;
		return;
	}
	// 对span的前后页,尝试进行合并,缓解内存碎片问题
	// 1、向前合并
	while (true)
	{
		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;

		// 将prevSpan从双向链表中移除
		_spanLists[prevSpan->_n].Erase(prevSpan);

		delete prevSpan;
	}
	// 向后合并
	while (true)
	{
		PAGE_ID nextID = span->_pageID + span->_n;
		auto ret = _idSpanMap.find(nextID);
		// 后面的页号没有,停止向后合并
		if (ret == _idSpanMap.end())
		{
			break;
		}
		// 后面的页号对应的span正在被使用,停止向后合并
		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}
		// 合并出超过128页的span无法进行管理,停止向后合并
		if (nextSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}
		// 进行向后合并
		span->_n += nextSpan->_n;

		// 将nextSpan从双链表中移除
		_spanLists[nextSpan->_n].Erase(nextSpan);

		delete nextSpan;
	}
	// 将合并后的span挂到对应的双链表中
	_spanLists[span->_n].PushFront(span);
	// 建议该span与其首位页的映射
	_idSpanMap[span->_pageID] = span;
	_idSpanMap[span->_pageID + span->_n - 1] = span;
	// 将该span设置为未使用状态
	span->_isUse = false;
}

向前合并 

说明:直接向堆申请内存时我们调用的接口是VirtualAlloc,与之对应的将内存释放给堆的接口叫做VirtualFree,而Linux下的brk和mmap对应的释放接口叫做sbrk和unmmap。此时我们也可以将这些释放接口封装成一个叫做SystemFree的接口,当我们需要将内存释放给堆时直接调用SystemFree即可。

// 将申请的空间释放给堆
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
	VirtualFree(ptr, 0, MEM_RELEASE);
#else
	// sbrk unmmap等
#endif
}

2.3、测试

下面我们对大于256KB的申请释放流程进行简单的测试:

void BigAlloc()
{
	void* p1 = ConcurrentAlloc(257 * 1024); // 257KB
	ConcurrentFree(p1, 257 * 1024);

	void* p2 = ConcurrentAlloc(129 * 8 * 1024); // 129页
	ConcurrentFree(p2, 129 * 8 * 1024);
}

当申请257KB的内存时,由于257KB的内存按页向上对齐后是33页,并没有大于128页,因此不会直接向堆进行申请,会向page cache申请内存,但此时page cache当中实际是没有内存的,最终page cache就会向堆申请一个128页的span,将其切分成33页的span和95页的span,并将33页的span进行返回

在释放内存时,由于该对象的大小大于了256KB,因此不会将其还给thread cache,而是直接调用的page cache当中的释放接口。 

由于该对象的大小是33页,不大于128页,因此page cache也不会直接将该对象还给堆,而是尝试对其进行合并,最终就会把这个33页的span和之前剩下的95页的span进行合并,最终将合并后的128页的span挂到第128号桶中。 



当申请129页的内存时,由于是大于256KB的,于是还是调用的page cache对应的申请接口,但此时申请的内存同时也大于128页,因此会直接向堆申请。在申请后还会建立该span与其起始页号之间的映射,便于释放时可以通过页号找到该span。 

在释放内存时,通过对象的地址找到其对应的span,从span结构中得知释放内存的大小大于128页,于是会将该内存直接还给堆。