007 高并发内存池_回收内存
前言
本文将会向你介绍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;
}
小结
今日的项目分享就到这里啦,做完一部分项目一定要好好总结呀,不能贪快,如果本文存在疏漏或错误的地方,还请您能够指出