CentralCache
目录
一、Span和Spanlist
二、CentralCache
一、Span和Spanlist
CentralCache其实也是哈希桶结构,只不过他是一个个的Span(Span是管理一定数量的页的结构),而且Span会管理一个freelist,用来挂起一个个的小内存块给ThreadCache使用。
同一个SpanList桶中的Span结构必须相同,除了freelist可以有不同的挂载情况。
SpanList的结构
这其中有一个字段叫做ustCount,他记录的就是该Span中的小内存块的使用情况,每给一个到ThreadCache,该字段就++,当ThreadCache调用ListTooLong归还内存块到CentralCache的时候再--。
一旦useCount被减到0了,则把该Span返还给PageCache。从而缓解内存碎片的问题。
当然CentralCache刚刚获取到Span的时候,useCount一定是0,那么万一此时刚好处于归还逻辑呢?岂不是又还回去了,白申请了。其实在Span中还有一个isuse表示是否正在被使用,就解决了这个问题。
//Span管理以页为单位的大块内存
struct Span
{
PAGE_ID _pageId = 0; //起始页的页号
size_t _pageN=0; //页的数量
size_t _useCount=0; //Span中有多少个小内存块被分配给ThreadCache了
void* _freelist=nullptr; //切好的小块内存块的自由链表,方便下一次分配给ThreadCache,不用在分配的时候切了,就好像定长池的memory
bool _isuse=false; //是否正在使用
size_t _objSize = 0; //小内存块的大小,即freelist中挂的一个个小对象节点的大小
Span* _prev = nullptr;
Span* _next = nullptr;
};
//带头双向循环链表
class SpanList
{
public:
SpanList()
{
_head = new Span();
_head->_next = _head;
_head->_prev = _head;
}
bool Empty()
{
return _head->_next == _head;
}
Span* PopFront()
{
Span* front = _head->_next;
Erase(front);
return front;
}
void PushFront(Span* span)
{
Insert(Begin(),span);
}
Span* Begin()
{
return _head->_next;
}
Span* End()
{
return _head;
}
void Insert(Span* pos,Span* newSpan)
{
assert(pos);
assert(newSpan);
//前Span
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos!=_head);
//前后Span
Span* next = pos->_next;
Span* prev = pos->_prev;
prev->_next = next;
next->_prev = prev;
}
std::mutex& GetMutex()
{
return _mutex;
}
private:
Span* _head=nullptr;
std::mutex _mutex; //桶锁
};
二、CentralCache
#pragma once
#include"Common.hpp"
#include"PageCache.hpp"
//单例模式---饿汉
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInstance;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t byte_size);
// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
private:
static CentralCache _sInstance;
CentralCache() {}
CentralCache(const CentralCache&) = delete;
private:
SpanList _spanlists[NFREELISTS];
};
//inline防止多个源文件包含导致重复定义
inline CentralCache CentralCache::_sInstance;
void CentralCache::ReleaseListToSpans(void* start, size_t byte_size)
{
//通过index可以知道该内存块是属于哪一个SpanList的
size_t index = SizeClass::Index(byte_size);
_spanlists[index].GetMutex().lock();
while (start!=nullptr)
{
void* next = NextObj(start);
//1.找到该内存块是属于哪一个Span
//当然可以通过内存块的地址,计算出页号,然后和SpanList中的Span的页号一个个比较,但是时间复杂度较高,采取哈希表映射
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
//2.把该内存块头插到Span的freelist中
NextObj(start) = span->_freelist;
span->_freelist = start;
span->_useCount--;
if (span->_useCount==0)
{
//该Span已经没有人用了,归还到PageCache中
_spanlists[index].Erase(span);
span->_freelist = nullptr;
span->_next = span->_prev = nullptr;
_spanlists[index].GetMutex().unlock();
PageCache::GetInstance()->GetMutex().lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->GetMutex().unlock();
_spanlists[index].GetMutex().lock();
}
//一个内存块还回去了,往后走一步
start = next;
}
_spanlists[index].GetMutex().unlock();
}
Span* CentralCache::GetOneSpan(SpanList& list, size_t byte_size)
{
//1.CentralCache有非空Span,直接取
Span* it = list.Begin();
while (it!=list.End())
{
if (it->_freelist!=nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
//把CentralCache的桶锁解开,其他线程释放内存的时候,就不会阻塞了
list.GetMutex().unlock();
//2.如果CentralCache中没有,找PageCache要
PageCache::GetInstance()->GetMutex().lock();
Span* span=PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(byte_size));
span->_objSize = byte_size;
span->_isuse = true;
PageCache::GetInstance()->GetMutex().unlock();
//3.把从PageCache中要到的Span,切分成小的内存块,挂到Span的freelist中
char* start = (char*)((span->_pageId) << PAGE_SHIFT); //起始地址(为什么pageId就是系统中的页号呢?为什么系统调用堆是从0页开始呢?)
size_t bytes = (span->_pageN) << PAGE_SHIFT; //总字节数
char* end = start + bytes;
//先头插一个
span->_freelist = start;
start += byte_size;
void* tail = span->_freelist;
//再把剩下的插入
while (start<end)
{
//尾插
NextObj(tail) = start;
tail = NextObj(tail);
start += byte_size;
}
NextObj(tail) = nullptr;
//插入到SpanList的时候,需要加锁,因为插入的动作是临界资源
list.GetMutex().lock();
list.PushFront(span);
return span;
}
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);
//加锁
_spanlists[index].GetMutex().lock();
//1.获取从非空的Span,从这个Span中获取自由链表中的内存块节点
Span* span = GetOneSpan(_spanlists[index],size);
assert(span);
assert(span->_freelist);
start = span->_freelist;
end = start;
size_t i = 0;
size_t actualNum = 1;
while(i<batchNum-1 && NextObj(end)!=nullptr)
{
end = NextObj(end);
i++;
actualNum++;
}
span->_freelist = NextObj(end);
NextObj(end)=nullptr;
//2.修改Span中的使用情况
span->_useCount += actualNum;
//解锁
_spanlists[index].GetMutex().unlock();
return actualNum;
}
值得注意的是:获取到span后我们要通过这个span的页数来知道这个span有多少内存,并且要通过这个span在程序地址空间的页号来判断这份内存的起始地址是多少!第0页的地址是0000 0000,第一页的地址是8KB,以此类推。
为什么从PageCache拿出Span给CentralCache的时候,需要建立一个页号和Span的映射关系呢?
因为PageCache的桶结构仍然是SpanList,但是和CentralCache不同的是:PageCache的桶中的Span是可以被修改的,即可以被切分成小的Span;而CentralCache的Span你开始的时候拿到的是多大的页,就永远是那么大了。
ThreadCache的内存块不被使用后,归还的时候可以计算出页号(页号通过小内存块的ptr指针计算)到了CentralCache,发现这个Span在不被使用后,归还给PageCache。而PageCache又会检查我的前后Span有没有归还,如果归还了,则可以把这两个Span合并成一个大的Span,从PageCache的若干个小桶移动到一个大桶中,从而缓解外碎片的问题。