1. thread cache释放内存
class ThreadCache
{
public:
// 释放内存对象
void Deallocate(void* ptr, size_t size);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELISTS];
};
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
1.1 void ThreadCache::Deallocate()
释放内存,首先需要根据对齐的字节数,去寻找在哪个桶内,将释放的内存重新挂在桶上。其次,需要判断一下现在挂在桶上的内存块是不是过于多,过于多时需要还给central cache,即还给span。
/释放空间需要告诉我 你属于哪个对齐数 然后挂到对应的哈希桶上 所以需要传入size
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
//找对映射的自由链表桶,将对象插入
size_t index = SizeClass::Index(size);
_freeLists[index].Push(ptr);
//当链表长度大于一次批量申请的内存时,就开始还一段list给central cache
if (_freeLists[index].Size() >= _freeLists[index].Maxsize())
{
ListTooLong(_freeLists[index], size);
}
}
1.2 void ThreadCache::ListTooLong()
桶上挂的内存块确实过于长时,需要先将一部分(桶的MaxSize()个数个)从桶结构中pop出来,所以在FreeList类里面新增一个接口,用于删除确定个数个的节点。
1.2.1 void FreeList::PopRange()
class FreeList
{
public:
void PushRange(void* start, void* end,size_t n)
{
NextObj(end) = _freeList;
_freeList = start;
_size += n;
}
//start 和 end为输出型参数
void PopRange(void*& start, void*& end, size_t n)
{
assert(n<=_size);
start = _freeList;
end = start;
for (size_t i = 0; i < n - 1; i++)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
size_t Size()
{
return _size;
}
size_t& Maxsize()
{
return _maxSize;
}
private:
void* _freeList=nullptr;
size_t _maxSize = 1;//用于辅助判断申请个数
size_t _size = 0;
};
1.2.2 函数实现
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.Maxsize());
//将一个一个切好的小内存 放到所属页的span里面
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
2.central cache 回收内存
class CentralCache
{
public:
static CentralCache* GetInstance()
{
return &_sInst;
}
//将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
private:
static CentralCache _sInst;
SpanList _spanLists[NFREELISTS];
};
可以看到thread cache还回来的内存仅有开始位置的地址。有开始位置的地址,就可以知道他属于 哪个页,即_pageId,但是我怎么知道_pageId是属于哪一个span的呢?由于span的共有成员中有起始页的_pageId和页数,按道理来说遍历一遍spanLists 也可以解决问题,但是这样效率太慢。
所以需要在分割Span时,就采用哈希表建立_pageId和Span的映射图。
当程序刚启动时,要想分配内存,首先需要pagecache调用NewSpan()函数,所以在该函数内我们需要添加_pageId和span的映射关系。
所以我们在page cache中新增一个私有成员,_idSpanMap 以及一个接口
Span* MapObjectToSpan(void* obj); 用于获取映射关系。
2.1 储备工作
2.1.1 _idSpanMap 和 建立映射关系
可以知道_pageId,但是不知道是哪个Span,如果采用遍历的方式找,效率太低。
class PageCache
{
public:
Span* NewSpan(size_t k);
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
private:
//新增
std::unordered_map<PAGE_ID, Span*> _idSpanMap;
};
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//在page cache中 页号和哈希桶的下标是对应的
if (!_spanLists[k].Empty())
{
Span* kSpan = _spanLists[k].PopFront();
//记录页号和span的映射 方便给central cache使用
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
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);
//nSpan是分割剩下的那一部分 整体挂在_spanLists 只映射头尾页号就可以使
//page Cache回收内存时 进行合并查找
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId+nSpan->_n -1] = nSpan;
//kSpan是要被thread cache用的 kSpan会被分割成小内存
//且还内存时 只知道指针 从而知道页号
//所以需要记录每个页号和span的映射
//方便将回收的小内存块挂对span
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
}
//走到这里 说明后面没有大页的span了
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
//4G的内存空间
//其实就是从0开始 一个一个的页构成的
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1;
_spanLists[bigSpan->_n].PushFront(bigSpan);
//现在要切分bigSpan了 但是按理来说不在项目里面写重复的代码
//所以采用递归调用
return NewSpan(k);
}
2.1.2 有关kSpan 和nSpan映射关系不需要统一的解释
nSpan是分割剩下的那一部分,整体挂在_spanLists,是闲置的。
当PageCache回收内存(后面会讲到)的时候,会对前后相邻页的不同span,进行检查并尝试合并一个更大的span,nSpan是不用的,连续的一大段空间,只要头尾被映射就可以拿到这个nSpan尝试合并。
kSpan是要被thread cache用的 kSpan会被分割成小内存,在我们上述实现的释放函数中,小内存的归还内存,只知道指针,而指针最多帮助我们知道页号,_spanLists对应的桶里面那么多span,我们怎么知道还到哪一个span里面呢?
所以需要记录每个页号和span的映射,方便将回收的小内存块挂对span。
2.1.3 Span* PageCache::MapObjectToSpan()
//传入指针 返回该指针在哪个span 这里访问了临界资源区 _idSpanMap
//这个哈希表在使用的时候必须是原子性的
Span* PageCache::MapObjectToSpan(void* obj)
{
//地址除以8K 可以得到在哪个页
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
std::unique_lock<std::mutex> lock(_pageMtx); //RAII的锁 出了作用域就会释放锁
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
2.2 void CentralCache::ReleaseListToSpans()
确定在_spanLists的哪个下标处,确定span
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
//先看看在_spanLists的几号下标处
size_t index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();
while (start)
{
void* next = NextObj(start);
//查看start在哪个span里面
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
NextObj(start) = span->_freeList;
span->_freeList = start;
span->_useCount--;
if (span->_useCount == 0)
{
//全都还回来了 把当前的span回收给page cache进行前后合并成一个大的span
_spanLists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
//后面要交给 page cache去进行操作
//把桶锁解除掉 以方便其他进程申请或者释放span
_spanLists[index]._mtx.unlock();
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
start = next;
}
_spanLists[index]._mtx.unlock();
}
3.PageCache 整理合并内存
依赖什么来合并呢?_useCount吗?但是在线程同时运行时,当一个span刚被切分时,他的_useCount就是0,可能会被直接拿来进行合并。
void PageCache::ReleaseSpanToPageCache(Span* span)
不合理,所以在span类里面再新增一个类型bool _isUse,用来看看当前这个span是否是被使用的,初始化默认为false,当span被切分时,修改 _isUse为true,那什么时候再将_isUse置为false呢?
一开始程序内部桶全是空,我们需要通过NewSpan()函数去申请一个128页的span,然后将kSpan(被申请的那部分)的_isUse置成true,而nSpan._isUse依然是默认值,即false。
也就是说在执行当前代码的时候span的_isUse一直是true,当他已经尝试合并前后页后,才置成false
struct Span
{
PAGE_ID _pageId=0;
size_t _n=0;
Span* _next=nullptr;
Span* _prev=nullptr;
size_t _useCount=0;
void* _freeList=nullptr;
bool _isUse = false;
};
3.1 合并
合并的整体流程
由于thread cache的释放,导致central cache回收其中过多的内存块,在这个过程中,某个span的_useCount减为0,那么这个span的所有分出去的内存小块就全回来了,所以继续把它往上交给PageCache,进行前后页的合并。
合并逻辑
首先在NewSpan()函数中,针对于空闲的页已经设置了映射关系,这时想找前后页,只需要在_idSpanMap中通过他的_pageId,找到前后页所在的span。
针对于向前合并,prevId=span->_pageId-1;通过prevId找到prevSpan,判断prevSpan._isUse是否为false,如果是则开始合并。合并即让span._pageId赋值成prevSpan._pageId,然后_n等变化。最后记得delete掉不用的span
向后合并同理,不再赘述。
3.2 void PageCache::ReleaseSpanToPageCache()
Span* PageCache::MapObjectToSpan(void* obj)
{
//地址除以8K 可以得到在哪个页
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//对前后相邻页的span 尝试合并一个更大的span
// 在NewSpan()函数中 分割的时候已经保存了 页数 和 span的映射关系
//向前合并
while (1)
{
//通过相邻的页数找 连续的空间 将两个在物理内存上连续的span 合成一个
PAGE_ID prevId = span->_pageId - 1;
//先看一下前面的页在不在
auto ret = _idSpanMap.find(prevId);
if (ret == _idSpanMap.end())
{
//不在 不能向前合并
break;
}
Span* prevSpan = ret->second;
if (ret->second->_isUse == true)
{
//前面的 相邻的 span在被使用 不能合并
break;
}
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_pageId = prevSpan->_pageId;
span->_n += prevSpan->_n;
_spanLists[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
//向后合并
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end()) break;
Span* nextSpan = ret->second;
if (nextSpan->_isUse == true) break;
if (nextSpan->_n + span->_n > NPAGES - 1) break;
span->_n += nextSpan->_n;
_spanLists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
_spanLists[span->_n].PushFront(span);
//当一个Span被使用时_isUse就变成了true 也就是说在执行当前代码的时候他的_isUse一直是true
//当他已经尝试合并前后页后,才置成false
span->_isUse = false;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
}