page cache
- 1 page cache的框架
- 2 central cache从page cache申请n页span的过程
- 3 page cache 的结构
- 3.1 page cache类框架
- 3.2 central cache向page cache申请span
- 3.3 获取k页的span
page cache
的结构和central cache
是一样的,都是哈希桶的结构,并且挂载的都是span
。但是不同的是,central cache
为thread cache
服务,分配的是一个个freeList
,所以需要和thread cache
的映射规则一样,page cache
为central cache
服务,分配的是一个个span
。所以page cache
挂载的span
不会进行切分。
1 page cache的框架
下面的图中,左边是central cache的结构,右边是page cache的结构。
我们假设page cache
的桶为128页
大小(这个可以根据实际情况更改),为了让桶号和页号对应起来,我们让0号桶
为空,所以page cache
的哈希桶个数要设置为129
.
//page cache中哈希桶的个数
static const size_t NPAGES = 129;
2 central cache从page cache申请n页span的过程
下面是page cache的结构
当central cache
向page cache
申请n页
的span
时,page cache
的做法如下:
3 page cache 的结构
1. page cache是否需要加锁?
需要。因为每个线程都拥有一份
thread cache
,当thread cache
中没有内存时,会向central cache
中要。如果多个thread cache
同时访问central cache
的一个桶时,就会出现线程安全问题,就需要加桶锁。但是如果多个thread cache
访问不同的central cache
是不会出现线程安全问题的,也不需要加锁。而如果此时多个central cache
同时访问page cache
就会出现线程安全问题。
2. 那page cache加的锁是桶锁吗?
不是
central cache
的分配方式是每个桶独立的,所以可以使用桶锁。而根据上面描述的page cache
的分配方式,访问第n号桶很可能是从n + 1
号桶中拿到span
,分配给central cache
后,再将剩余的span
挂载到1号桶中。这种分配方式会涉及到多个桶,如果使用桶锁,锁住其中一个桶,其他的桶就无法进行协同了。所以page cache
加的是一把大锁,能将整个page cache
锁住。
4.page cache需要设计单例模式吗?
需要。整个进程中能有一个
page cache
,所以page cache
也需要设计一个单例模式。
3.1 page cache类框架
//单例模式
class PageCache
{
public:
//提供一个全局访问点
static PageCache* GetInstance()
{
return &_sInst;
}
private:
//static const size_t NPAGES = 129;
SpanList _spanLists[NPAGES];
std::mutex _pageMtx; //大锁
private:
PageCache() //构造函数私有
{}
PageCache(const PageCache&) = delete; //防拷贝
static PageCache _sInst;
};
3.2 central cache向page cache申请span
当thread cache
向central cache
申请span
的时候,会从central cache
中获取一个非空的span
,在上篇文章中的实现是这样的:
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 查看当前的spanlist中是否有还有未分配对象的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
list._mtx.unlock();
return span;
}
但是上篇文章没有考虑如果central cache
中没有span
了该怎么办。
当central cache中没有span了该怎么办?
需要向
page cache
申请
central cache如何向page cache申请span?
1.计算出
thread cache
一次向central cache
申请的freeList
的个数num
2. 将freeList
的个数num
和thread cache
申请的size
字节相乘,计算出要申请的总字节数npage
3. 将npage
除以页的大小,最终得到central cache
要向page cache
申请多少个页。不满1页按1页算。
static size_t NumMovePage(size_t size)
{
//计算thread cache找central cache要的freeList的个数
size_t num = NumMoveSize(size);
//计算申请的总的字节数
size_t npage = num*size;
//计算向page cache申请的总的页数
npage >>= PAGE_SHIFT;
//不满1页按1页算
if (npage == 0)
npage = 1;
return npage;
}
PAGE_SHIFT
表示页的大小,这里为13,因为2<sup>13</sup> = 8KB
static const size_t PAGE_SHIFT = 13;
当central cache
从page cache
中申请到span
之后,还需要将span
进行切分,切分成符合大小的freeList
挂在span
下面。
central cache是如何进行切分的?
1.计算
span
的大块内存的起始地址star
t:span
的起始页号乘以一页的大小即可得到这个span
的起始地址
2.计算span
的大小(字节数)bytes
:span
的页数乘以一页的大小
3.计算span的结束位置的地址end
:start + bytes
4.将span切分为size大小,全部尾插到原来的span->freeList后面。具体过程看下图。
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
// 查看当前的spanlist中是否有还有未分配对象的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
// 先把central cache的桶锁解掉,这样如果其他线程释放内存对象回来,不会阻塞
list._mtx.unlock();
// 走到这里说没有空闲span了,只能找page cache要
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
span->_isUse = true;
span->_objSize = size;
PageCache::GetInstance()->_pageMtx.unlock();
// 对获取span进行切分,不需要加锁,因为这会其他线程访问不到这个span
// 计算span的大块内存的起始地址和大块内存的大小(字节数)
char* start = (char*)(span->_pageId << PAGE_SHIFT);
size_t bytes = span->_n << PAGE_SHIFT;
char* end = start + bytes;
// 把大块内存切成自由链表链接起来
// 1、先切一块下来去做头,方便尾插
span->_freeList = start;
start += size;
void* tail = span->_freeList;
int i = 1;
while (start < end)
{
++i;
NextObj(tail) = start; //相当于tail->next = start
tail = NextObj(tail); // tail = start;
start += size;
}
NextObj(tail) = nullptr;
// 切好span以后,需要把span挂到桶里面去的时候,再加锁
list._mtx.lock();
list.PushFront(span);
return span;
}
3.3 获取k页的span
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
上面是GetOneSpan
函数中用到的从page cache
中拿k页大小的span
的调用过程。
当central cache
向page cache
申请一个k页的span
时,如果page cache
中有k页的span
,就直接删除然后给central cache
就行了,如果没有,就要去堆中申请一个128页大小的span
了。
回顾之前central cache向page cache申请内存的过程。
当central cache向page cache申请一个k页大小的span时,由于page cache是直接映射,所以只需要去第k号桶中找是否有span就行了。如果没有,就去k + 1,k + 2…中找,直到找到第128号桶都没有时,再去堆中申请。
当page cache
向堆中申请128页大小的内存时,得到的是128页内存的起始地址,我们需要将这份起始地址转换为对应的页号,也就是将起始地址除以1页的大小。
//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//先检查第k个桶里面有没有span
if (!_spanLists[k].Empty())
{
return _spanLists[k].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页下来
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
nSpan->_pageId += k;
nSpan->_n -= k;
//将剩下的挂到对应映射的位置
_spanLists[nSpan->_n].PushFront(nSpan);
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);
}
为什么要递归调用?
因为
page cache
向堆申请了128页大小的内存,需要将其切分成k页的span和128-k页的span。本来需要编写切分的代码,但是为了避免写重复的代码,就再递归调用该函数,后面会自行切分的。
当central cache向page cache申请内存时,central cache对应的哈希桶是处于加锁的状态的,那在访问page cache之前我们应不应该把central cache对应的桶锁解掉呢?
这里建议在访问
page cache
前,先把central cache
对应的桶锁解掉。虽然此时central cache
的这个桶当中是没有内存供其他thread cache
申请的,但thread cache
除了申请内存还会释放内存,如果在访问page cache
前将central cache
对应的桶锁解掉,那么此时当其他thread cache
想要归还内存到central cache
的这个桶时就不会被阻塞。
因此在调用
NewSpan
函数之前,我们需要先将central cache
对应的桶锁解掉,然后再将page cache
的大锁加上,当申请到k页的span
后,我们需要将page cache
的大锁解掉,但此时我们不需要立刻获取到central cache
中对应的桶锁。因为central cache
拿到k页的span
后还会对其进行切分操作,因此我们可以在span
切好后需要将其挂到central cache
对应的桶上时,再获取对应的桶锁。
这里为了让代码清晰一点,只写出了加锁和解锁的逻辑,我们只需要将这些逻辑添加到之前实现的
GetOneSpan
函数的对应位置即可。
spanList._mtx.unlock(); //解桶锁
PageCache::GetInstance()->_pageMtx.lock(); //加大锁
//从page cache申请k页的span
PageCache::GetInstance()->_pageMtx.unlock(); //解大锁
//进行span的切分...
spanList._mtx.lock(); //加桶锁
//将span挂到central cache对应的哈希桶