1.page cache介绍
1.1 申请内存设计
page cache本质也是哈希桶结构,挂着的也是spanList,但映射关系和之前二者就不同了,设计共有128个桶,桶下面挂着与桶编号有关的span数,0号桶下面挂着的是1个1个的page span,3号桶下面挂着的是3个3个的page span...
当程序运行起来时,page cache桶结构全是空,它会用一定的方法向系统申请128页page span,挂在自由链表中。当central cache向page cache申请内存时,page cache 先检查对应位置有没有span,如果没有就会向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找,假设在第10页找到了一个span,则将其分裂为一个4页page span和一个6页page span
1.2锁的设计
桶锁可以解决线程同时访问一个span的问题,但是拿走页之后pagecache会分割页,然后将不用的挂起来,桶锁不能锁住这一临界区,是不安全的,所以只能采用一把大锁,锁住pagecache对象。
而且,central cache可以使用桶锁的原因是会去指定的地方寻找span,只需要访问该桶,但是对于page cache是需要由page span从小到大遍历,去寻找一个非空,然后将其分割。如果使用桶锁,每进入一个桶就要发生加锁和解锁过程,增多不必要的消耗。
1.3 合并整理设计
如果central cache中span usecount等于0,说明切分给thread cache小块的内存都回来了,则central cache把这个span还给page cache,page cache通过页号,查看前后的相邻页是否空闲,是就合并,合并成更大的页,解决内存碎片问题。
2. PageCache的实现
声明
//因为所有的线程都从一个central cache中获取值 所以我们将它设计为单例模式
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
//从中心缓冲中获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
// 获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t byte_size);
private:
CentralCache()
{}
CentralCache(const CentralCache& abc) = delete;
private:
static CentralCache _sInst;//记得类外初始化
SpanList _spanLists[NFREELISTS];
};
2.1 Span* CentralCache::GetOneSpan()
在上篇博文中,我们留下了一个问题 就是GetOneSpan()的实现,现在既然有了CentralCache的定义,所以这里先构思一下这个函数的实现。
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
首先,寻找一个非空的Span,有两种可能
一、从 _spanLists中遍历,找到一个span a,如果a._freeList不为空就把a返回
二、_spanLists没有空闲的span了,只能找page cache要。那要多少呢?不能每次都一次要一个吧?
2.1.1 _spanLists的遍历
SpanList类是一个双向循环的带头链表,要想遍历整个链表就需要确定起始位置和终止位置,所以在SpanList类中需要新增两个接口,用于返回起始位置和结束位置。
class SpanList
{
public:
//...
Span* Begin()
{
return _head->_next;
}
Span* End()
{
return _head;
}
//后续会用到
void PushFront(Span* span)
{
Insert(Begin(), span);
}
Span* PopFront()
{
Span* Pop = _head->_next;
Erase(Pop);
return Pop;
}
//...
private:
Span* _head=nullptr;
};
2.1.2 SizeClass::NumMovePage()
设计一个期望拿到页数的函数。
class SizeClass
{
pblic:
//写过的略 ..
//计算一次从page cache获取几个页
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;//右移13位 相当于/=8k
if (npage == 0) npage = 1;
return npage;
}
};
2.1.3 函数实现
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//先查看当前的 _spanLists看看还有没有未满的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
//现在只能向page cache中申请了
/*
* 桶锁解不解呢? 对于申请来讲,假如一号线程在1号桶内没有发现空余的span跑去page cache申请
* 那二号线程在访问1号桶的时候,无论解锁锁与不解锁他都不能往下执行,均可。
* 但是对于释放来讲,他只是将回收的内存挂在1号桶,如果不解锁,这个过程需要等到1号线程申请
* 完毕后才能进行,程序的效率会下降。
*/
list._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
PageCache::GetInstance()->_pageMtx.unlock();
//拿到了 开始切分 不需要加锁 因为还没有挂到桶上 其他线程拿不到这个span
char* start = (char*)(span->_pageId << PAGE_SHIFT);//意思是用 第几个页*页的大小 算出来起始位置
char* end = start + (span->_pageId << PAGE_SHIFT);
span->_freeList = start;
//采用尾插的方式可以保证切好的内存空间是连续的
start += size;
void* tail = span->_freeList;
while (start < end)
{
NextObj(tail) = start;
tail = start;
start += size;
}
//记得尾指针置空
Nextobj(tail)=nullptr;
//将切好的span 挂起来 在临界区内 需要加锁 不能一边申请你一边往里面放
list._mtx.lock();
list.PushFront(span);
return span;
}
2.2 Span* PageCache::NewSpan( )
2.2.1 向堆申请一个128页的空间
在一开始时,page cache上所有的桶都是空,需要从堆申请一个大块的内存。
#ifdef _WIN32
#include <windows.h>
#else
// ...
#endif
// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
因为该函数返回的是一个void*的指针,我们怎么把该指针和页号联系在一起呢?
4G的内存空间本质上就是一个一个连续的页构成的,所以16进制的地址本质上也是从0开始的,递增的数字。所以将地址转为整数然后除以一个页的大小,得到的就是页的编号!
2.2.2 函数实现
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//在page cache中 页号和哈希桶的下标是对应的
if (!_spanLists[k].Empty())
{
return _spanLists[k].PopFront();
}
for (size_t i = k + 1; i < NPAGES; i++)
{
if (!_spanLists[i].Empty())
{
//取走该块大span
Span* nSpan = _spanLists[i].PopFront();
//创建一个新的span 即我们需要的页大小的span
Span* kSpan = new Span;//这里默认值给过了 都是设置好的
//现在需要将 nSpan的一部分切下来给 kSpan 采用从头切
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;//_pageId是 整个span起始页的编号
nSpan->_n -= k;
//我需要的是kSpan 那剩下的nSpan就要挂在对应下标的桶里面
_spanLists[nSpan->_n].PushFront(nSpan);
return kSpan;
}
}
//走到这里 说明后面没有大页的span了
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; //4G的内存空间 其实就是从0开始 一个一个的页构成的
bigSpan->_n = NPAGES - 1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
//现在要切分bigSpan了 但是按理来说不在项目里面写重复的代码
//所以采用递归调用
return NewSpan(k);
}