目录
- 前言
- 1. 页缓存的具体结构
- 2. 页缓存分配内存的全过程
- 3. 页缓存分配内存的代码实现
- 4. 优化代码,并完全脱离malloc
- 5. 总结以及代码拓展
前言
在页缓存这一层中,负责给中心缓存分配大块儿的内存,以及合并前后空闲的内存,这一层为整体加锁!
本章重点:
本篇文章着重讲解内存池第三层:页缓存的基本成员变量和函数,以页缓存的具体结构是怎样的。了解完基础结构后,会详解讲解中心缓存层来申请内存时的具体步骤!
1. 页缓存的具体结构
页缓存也是一个哈希桶结构,但它的映射规则和前两层不同,它的规则是:
K号桶中的大块儿内存就是K页
在pagecache.h文件中
// 1.central cache向page cache取,关注的是需要几页span
// page cache也是哈希桶,用的是直接定址
// 2.也使用单例饿汉模式
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 _spanList[NPAGES];
ObjectPool<Span> _spanPool; // 把使用new/delete的地方换成定长内存池的设计
// 有内存块的地址就可以计算出页号,再加上这个页号和span之间的映射
// 就可以找到内存块在哪个span
//std::unordered_map<PAGE_ID, Span*> _idSpanMap;
TCMalloc_PageMap1<32 - PAGE_SHIFT>_idSpanMap;
private:
PageCache()
{}
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
2. 页缓存分配内存的全过程
当中心缓存中没有内存时,会去页缓存申请一个span结构,要经过下面几步:
1.根据中心缓存的桶号来确定申请的span是几页的;
2.根据中心缓存想要申请的页数,找到页缓存中对应的桶(k页对应k号桶);
3.情况一: 页缓存的K号桶中存在span结构,直接将这块儿内存返回给中心缓存;
4.情况二: 页缓存的K号桶没有span结构,但是K+1到128号桶中存在span结构,假设n号桶有span,则将这个大页的span切分为一个k页的span和一个n-k页的span,k页的span返回给中心缓存去使用,而将n-k页的span重新挂在n-k号桶中;
5.情况三: k到128号桶都没有span,此时页缓存会向系统申请一份128页大小的内存,并挂在128号桶中,再将这个128页的span切分为k页的span和128-k页的span,也就转换为了情况二!
并且在这个过程中,页缓存将一个span分配给中心缓存后,会记录下来这块儿内存的页号和span的映射关系,方便后续回收内存的时候再使用!
3. 页缓存分配内存的代码实现
// 获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
// 假设central cache需要2page:
// 刚开始哈希桶为空,没有一个span,向堆申请一个128page 的span
// 把128page 切分成2page的span和126page的span
// 2page返回给central cache使用,126page挂到126号桶里
assert(k > 0);
// 如果大于128页,直接找堆要
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
Span* span = new Span;
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
_idSpanMap[span->_pageId] = span;
return span;
}
// 先检查第k个桶里有没有span
// 如果有,直接弹出去给上一层的central cache用于切成一个个小内存对象
if (!_spanList[k].Empty())
{
Span* kSpan = _spanList[k].PopFront();
// 建立pageid和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
//_idSpanMap[kSpan->_pageId + i] = kSpan;
_idSpanMap.set(kSpan->_pageId + i, kSpan);
}
return kSpan;
}
// 检查一下后面的桶里面有没有span,如果有,就可以把它进行切分
// 进而挂到page cache的不同桶里
for (size_t i = k + 1; i < NPAGES; i++)
{
if (!_spanList[i].Empty())
{
// 只要其中一个桶里有span,直接取下来进行切分
// 切分成一个k页的span和一个n-k页的span
// k页的span返回给central cache,n-k页的span挂到第n-k个桶
Span* nSpan = _spanList[i].PopFront();
//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();
// 在nSpan的头部切一个k页下来
// k页的span返回
kSpan->_pageId = nSpan->_pageId;
kSpan->_n = k;
// 原来桶里的span被切掉了k页,所以页号要往前加k,页数要减k
nSpan->_pageId += k;
nSpan->_n -= k;
// 再把剩下的页挂到对应的桶里
_spanList[nSpan->_n].PushFront(nSpan);
// 存储nSpan的首尾页号跟nSpan映射,方便page cache回收内存时,进行合并查找
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
// 建立pageid和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;
Span* bigSpan = _spanPool.New();
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; // 地址->页号的转换
bigSpan->_n = NPAGES - 1;
// 再把从系统上获取到的128页挂到第128个桶里
_spanList[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);
}
这个地方有一个设计的比较巧妙的点,那就是出现情况三的时候,向系统申请了128页的空间后,再次调用这个函数就一定会出现情况二,从而在for循环中走完整个过程。
4. 优化代码,并完全脱离malloc
细心的同学会发现,在这个函数中使用到了new操作符,然而了解new底层原理的同学应该知道,new的底层实际上是用的malloc来申请的空间,但是我们这个项目就是为了完全脱离malloc函数来实现一个多线程下高效的内存池,所以这里一定不能使用new!
使用之前编写的定长池来舍弃new!
首先, 在页缓存类中添加上一个成员变量: 定长池类, 然后在使用new的地方,把new全部替换为用定长池申请空间!
5. 总结以及代码拓展
页缓存分配内存的一环设计的是非常的巧妙,但是页缓存真正巧妙的地方是在合并空闲内存的一环!
对代码的拓展:
我们会发现页缓存结构中调用了好几次向系统申请内存的函数,这个地方只做了解,会用接口就行。
inline static void* SystemAlloc(size_t kpage)//申请kpage页内存
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << PAGE_SHIFT, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等直接向系统申请内存的方式
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}