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

💡Yupureki:个人主页
✨个人专栏:《C++》 《算法》《Linux系统编程》《高并发内存池》
🌸Yupureki🌸的简介:

目录
完整项目链接https://github.com/Yupureki-code/ConcurrentMemoryPool
1. 准备工作
当ThreadCache把内存块还给CentralCache时,这些内存块挂在哪里?我们知道这些内存块之前是在一个Span下的,而也应该理所应当还给那个Span

因此我们必须用哈希表记录一个内存块对应的Span。由于每个Span也都是PageCache给CentralCache的,对应关系也应该由PageCache知道,因此我们得在PageCache内新增一个内存块地址查找Span的哈希表
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函数中。因此该函数需要更新
// 获取一个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把内存收回后,会把内存块串在对应的哈希桶内
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
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还回来的内存块:
- 通过PageCache内的哈希表查找内存块对应的Span
- 串在对应的Span下
- 如果该Span没有任何内存块被ThreadCache拿走使用去了,直接还给PageCache
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还回来的内存块:
- 将该内存块向前和向后合并,一直到无法合并为止
- 合并后,串在对应的哈希桶内
- 合并后,对应的哈希映射关系需要清除
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; }