
1. 细节处理
1.1 细节1
我们这个项目本来就是要代替系统的内存分配相关函数malloc和free,但是在我们的代码中还是在通过new申请,delete释放。new底层封装了malloc,delete底层封装了free。还是没和malloc和free脱离。
这里我们就可以把项目最开始做的定长内存池拿过来用了。它是一个模板想要什么类型内存找它要就行了。下面就把用到new和delete的地方都换成去找定长内存池申请和释放。
//ObjectPool.h
template<class T>
class ObjectPool
{
public:
ObjectPool()
:_memory(nullptr), _remainBytes(0),_freelist(nullptr)
{
}
T* New()
{
T* obj = nullptr;
//如果链表中有空闲的内存块,就优先使用
if (_freelist)
{
//头删
void* next = *(void**)_freelist;
obj = (T*)_freelist;
_freelist = next;
}
else
{
//if (_memory == nullptr)
if(_remainBytes < sizeof(T))//如果当前内存块不够一个T类型,就重新申请一大快内存快
{
_remainBytes = 128 * 1024;
//_memory = (char*)malloc(_remain);
_memory = (char*)SystemAlloc(_remainBytes >> 13);//按页申请,想要每页8kb,申请多少页
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
obj = (T*)_memory;
//保证内存块至少能放下一个地址
size_t Size = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += Size;
_remainBytes -= Size;
}
// 定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
//对释放的内存块进行头插
// 显示调用析构函数清理对象
obj->~T();
//直接头插
* (void**)obj = _freelist;
_freelist = obj;
}
private:
char* _memory;//指向申请的一大块内存
size_t _remainBytes;//记录申请一大块内存还剩下多少内存
void* _freelist;//指向释放后的内存,以链表形式管理起来
};
//PageCache.h
#include"Common.h"
#include"ObjectPool.h"
//所有线程共享一个PageCache
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//从PageCache第k号桶中获取一个span对象
Span* NewSpan(size_t k);
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
private:
PageCache()
{
}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
private:
Spanlist _spanlists[NPAGES];
std::unordered_map<PAGE_ID, Span*> _idSpanMap;//记录页号和span一一对应关系
ObjectPool<Span> _spanPool; //申请Span的地方都用定长内存池申请,不用new
public:
std::mutex _pageMtx; //整个锁
//PageCache.cpp
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
//assert(k > 0 && k < NPAGES);
assert(k > 0);
//大于 128page找堆要
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//申请一个span对象管理这块内存,释放内存的时候要对应的span
//Span* span = new Span;
Span* span = _spanPool.New();
span->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
_idSpanMap[span->_pageid] = span;
return span;
}
//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁
// 先检查第k个桶里面有没有span
if (!_spanlists[k].Empty())
{
Span* Kspan = _spanlists[k].PopFront();
//将Kspan给CentralCache之前,先将页号和span对应关系记录
for (size_t i = 0; i < Kspan->_n; ++i)
{
_idSpanMap[Kspan->_pageid + i] = Kspan;
}
//将管理k页的span给Central Cache
return Kspan;
}
//走到这里第k个桶没有span,那就继续往下面的桶找
for (size_t i = k + 1; i < NPAGES; ++i)
{
if (!_spanlists[i].Empty())
{
//将一个管理更多页的span,变成两个span,一个管理k页,一个管理i-k页
Span* Ispan = _spanlists[i].PopFront();
//Span* Kspan = new Span;
Span* Kspan = _spanPool.New();
Kspan->_pageid = Ispan->_pageid;
Kspan->_n = k;
Ispan->_pageid += k;
Ispan->_n -= k;
//将Ispan挂在对应大小的桶
_spanlists[Ispan->_n].PushFront(Ispan);
//记录挂在PageCache中还未被使用的Ispan的页号和span对应关系
//这里仅需记录这个span的首页和尾页与Ispan的对应关系即可,
//不像返回给CentralCache的Kspan的需要把这个Kspan管理的每一页都和Kspan建立映射关系
//因为合并仅需知道每个Ispan的首页和尾页就可以找到Ispan,而返回给CentralCache的Kspan,
//首先需要将Kspan切成一块块小内存才行才能再给ThreadCache用,
//当小内存回来/8kb可能是Kspan管理的其中某一页,才能知道该页对应span
_idSpanMap[Ispan->_pageid] = Ispan;
_idSpanMap[Ispan->_pageid + Ispan->_n - 1] = Ispan;
//将Kspan给CentralCache之前,先将页号和span对应关系记录
for (size_t i = 0; i < Kspan->_n; ++i)
{
_idSpanMap[Kspan->_pageid + i] = Kspan;
}
//将管理k页的span给Central Cache
return Kspan;
}
}
//走到这里说明,后面的位置都没有大页的span
//那就去找堆申请一个128page的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;
//span挂在对应桶下
_spanlists[bigSpan->_n].PushFront(bigSpan);
//重复上述过程寻找k号桶的span,一定还回一个span
return NewSpan(k);
}
// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于 128page直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
SystemFree(ptr);
//delete span;
_spanPool.Delete(span);
return;
}
// 如何知道相邻的span是否能合并?
// 通过自己的页号找到相邻的span看是否能合并,如果该页的span在PageCache说明该span还没有被使用,可以合并
// 如果在CentralCache说明该span正在被使用,不能合并
// 如何知道一个span是否被使用? 是用span中的usecount是否等于0吗? 不能!!
// 这里有一个空档时间,当thread1线程通过TLS找到自己threadcache申请内存块,但是没有,
// 就去找CentralCache,但是CentralCache对应桶下也没有,那就只能去找PageCache了
// PageCache返回给CentralCache一个span,这个span的usecount初始可是0,
// CentralCache拿到后对span这一大块内存切成一块块小内存
// 在挂到对应桶下,但是这时候thread2,要合并这个span,那就有问题了,thread1正准备从这span拿一批量
// 但是还没有拿到,这个span的usecount可还是0,只有拿走了usecount才会++
// thread2把这个span和自己span合并了,那就造成线程安全的问题!!
// 因此需要给span对象加一个isuse成员记录这个span是否被使用
// 如何通过页号找到相邻的页? 还是得用unordered_map记录页号合span对应关系
// 但是目前的unordered_map只记录了给CentralCache已经被使用的span的页号和span对应关系
// 并没有记录在PageCache的span的页号和span对应关系
// 因此需要把在PageCache的span的页号和span对应关系也要记录在unordered_map中
//先走前面相邻的span是否能合并,能合并就一直合
while (1)
{
PAGE_ID prevId = span->_pageid - 1;
auto ret = _idSpanMap.find(prevId);
// 前面的页号没有,不合并了(堆申请内存已经到了起始地址)
if (ret == _idSpanMap.end())
{
break;
}
Span* prevSpan = ret->second;
// 前面相邻页的span在使用,不合并了
if (prevSpan->_isuse == true)
{
break;
}
// 合并出超过128页的span没办法挂在桶下,不合并了
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
// 用span合并prevSpan
span->_pageid = prevSpan->_pageid;
span->_n += prevSpan->_n;
// 将pevSpan从对应的桶中删除
_spanlists[prevSpan->_n].Erase(prevSpan);
//delete prevSpan;
_spanPool.Delete(prevSpan);
// 这里可能有疑问,那遗留unordered_map中被合并的对应页和prevSpan之间一对一的关系难道不删除吗?
// 因为prevSpan已经被删除了,在去通过已有页去找span那就是野指针了! 但其实并不用删除.
// 首先被合并的页已经被span管理起来了,合并结束之后会被挂在对应桶下,并且记录该span首页和尾页与span的对应关系.
// 当CentralCache要的时候,在把span切分成两个span,返回给CentralCache的Kspan每页都和Kspan重新进行映射
// 留在PageCache的Ispan的首页和尾页也会和Ispan重新映射
// 这样的话,以前被合并,遗留下来的页又和新得span建立了映射关系,就不会有通过页找span会有野指针的问题
}
//找后面相邻的span合并
while (1)
{
PAGE_ID nextId = span->_pageid + span->_n;
auto ret = _idSpanMap.find(nextId);
// 后面的页号没有,不合并了(堆申请内存已经到了结尾地址)
if (ret == _idSpanMap.end())

最低0.47元/天 解锁文章
2199





