点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1. 释放内存
1.1 thread cache
释放内存:
- 当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freelists[i]。
- 当链表的长度过长,则回收⼀部分内存对象到central cache。
//ConcurrentAlloc.h
static void ConcurrentFree(void* ptr,size_t size)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
以什么标准来判断ThradCache当前某个桶的自由链表的长度过长了呢?
我们在给ThreadCache的桶加一个Size成员,记录当前这个桶空闲内存块的个数。如果当前这个桶空闲内存块的个数大于等于这个桶下一次找CentralCache要的一批量的内存块个数。那就把这一批量从当前桶拿到然后还给CentralCache。
其实还可以给ThreadCache一个记录它占了多大内存的成员,如果这个遍历大于2M,就从上到下遍历桶还一部分空闲内存块回去,避免一个ThreadCache占据太多内存。我们这个项目的原型考虑了更多这里的细节。我们就简单一下用个Size记录当前桶空闲内存块个数就行了。
//Common.h
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
class Freelist
{
public:
//释放内存
void Push(void* obj)
{
//头插
assert(obj);
NextObj(obj) = _freelist;
_freelist = obj;
++_size;
}
//申请内存
void* Pop()
{
//头删
assert(_freelist);
void* obj = _freelist;
_freelist = NextObj(obj);
--_size;
return obj;
}
//一次挂一批
void PushRange(void* start,void* end,size_t n)
{
NextObj(end) = _freelist;
_freelist = start;
_size += n;
}
//一次拿一批
void PopRange(void*& start, void*& end, size_t n)
{
assert(_size >= n);
start = end = _freelist;
for (size_t i = 0; i < n - 1; ++i)
{
end = NextObj(end);
}
_freelist = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
//链表是否为空
bool IsEmpty()
{
return _freelist == nullptr;
}
//获取当前桶获取下一批内存块的个数
size_t& GetMaxSize()
{
return _maxSize;
}
//当前桶的空闲内存块的个数
size_t GetSize()
{
return _size;
}
private:
void* _freelist = nullptr;
size_t _maxSize = 1;//记录当前桶下一次找ContraltCache一次要一批量是多少个内存对象
size_t _size = 0;//当前桶空闲内存块个数
};
//ThreadCache.h
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(Freelist& list, size_t size);
private:
Freelist _freelists[NFREELIST];
};
// TLS thread local storage 线程局部存储
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
//ThreadCache.hpp
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
size_t index = SizeClass::Index(size);
_freelists[index].Push(ptr);
//当前桶空闲内存块个数大于等于下一次申请一批量内存块个数
//就还下一批量内存块的个数给CentralCache
if (_freelists[index].GetSize() >= _freelists[index].GetMaxSize())
{
//从当前桶获取一批量内存块
ListTooLong(_freelists[index], size);
}
}
void ThreadCache::ListTooLong(Freelist& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//获取一批量内存块对象
list.PopRange(start, end, list.GetMaxSize());
//将这一批内存块对象还给CentralCache对应桶下的对应Span对象
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
1.2 central cache
释放内存:
- 当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时 --use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并。
//CentralCache.h
//所有线程共享,整个进程仅有一个,所有弄成单例模上:饿汉(静态对象)
class CentralCache
{
public:
//获取单例对象
static CentralCache* GetInstance()
{
return &_sInst;
}
// 获取一个非空的span
Span* GetOneSpan(Spanlist& list, size_t size);
// 从中心缓存获取一定数量的内存块对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
// 将一定数量的内存块对象释放到对应桶下的对应span对象中
void ReleaseListToSpans(void* start, size_t size);
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
CentralCache& operator=(const CentralCache&) = delete;
private:
Spanlist _spanlists[NFREELIST];
static CentralCache _sInst;
};
考虑这样一个问题,ThreadCache还回来的内存块还到CentralCache对应桶中,但是对应桶可能有多个Span对象,那这些还回来的内存块对象是属于那个Span呢?
比如说目前有两个Span对象,分别管理一页,2000是Span1管理,2001是Span2管理。下面有四个内存块还回来了?假设1、2内存块是从Span1拿的,3、4是从Span2拿的。那怎么确定对应内存块是从那个Span中拿的呢?
以1、2内存块从Span1拿的为例,既然是从Span1管理2000这一页拿,这一页是划分为固定大小的小内存块你才拿的。那1、2这个内存块的地址是不是就在FA000和FA2000之间的,拿它们的地址/8KB算出的整数还是2000。
因此我们可以用unordered_map或者map建立一个页号与Span一对一的映射关系。拿到内存块的地址/8KB 看它是那一页的,然后根据页号就可以知道它是那个Span了。
这里我们使用unordered_map查找更快一些,这个哈希桶等会PageCache也要用,因此放在PageCache里。
//PageCache.h
#include"Common.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一一对应关系
public:
std::mutex _pageMtx; //整个锁
};
然后在处理一下,当CentralCache从PageCache拿到一个Span,返回之前,我们要把这个Span管理的页号与Span建立关系。
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁
// 先检查第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);
//将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;
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的函数
// 获取从对象到span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
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;
}
}
接下来CentralCache就可以把一批内存块还给PageCache了。这里也需要记住在取PageCache之前先把对应桶锁解掉。不如别的线程来对应桶上申请释放就阻塞了。
// 将一定数量的内存块对象释放到对应桶下的对应span对象中
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
size_t index = SizeClass::Index(size);
_spanlists[index]._mtx.lock();
//遍历将内存块对象还给对应的span
//如何知道一个内存块对象是从那个span对象中拿走的?
//用内存块中的地址/8kb,得到对应的页号,根据页号走到对应的span
//如何根据页号找到对应span,用unordered_map记录页号和span一一对应关系
//因为PageCache等会也要用到这个哈希桶,因为哈希桶定义到PageCache中
while (start)
{
void* next = NextObj(start);
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
//将start头插进对应span中
NextObj(start) = span->_freelist;
span->_freelist = start;
span->_usecount--;
//检查当前span中切分的内存块个数是否已经被还完了
//如果还完,就将该span从CentralCache中删除,然后还给PageCache
//PageCache看是否能将该span前后空闲span合并,减少内存碎片
if (span->_usecount == 0)
{
//将该span从桶中删除
_spanlists[index].Erase(span);
span->_freelist = nullptr;
span->_prev = nullptr;
span->_next = nullptr;
//从CentarlCache去PageCache之前先把桶锁释放
//如果不释放,万一其他线程来这个桶找span申请或者释放内存块,就阻塞住
_spanlists[index]._mtx.unlock();
//所有线程共享PageCache,所以先加锁
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
//回来再把对应桶锁加上
_spanlists[index]._mtx.lock();
}
start = next;
}
_spanlists[index]._mtx.unlock();
}
1.3 page cache
释放内存:
- 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
假设Span管理的页是起始页是2000,一共管理5页,和它相邻的前后Span如何找到?
和它的前一个相邻的Span最后一页就是1999,后一个相邻的Span的首先就是2005。我们还是可以通过找到页号就可以找到对应的Span。
合并相邻的页,通过页号找到相邻的页看是否能合并,如果该页的Span在PageCache说明该Span还没被使用可以合并。如果在CentralCache说明该Span正在被使用不能合并。
那如何确定一个Span是否在被使用呢?能不能用Span中的_usecount==0来判断?
不能!因为有时间间隔。如果线程thread1找PageCache拿到一个Span,记住_usecount初始可是0,然后切分成一块块小内存块,在挂到CentralCache对应桶中,然后正准备从这个Span拿一批内存块,这个时候_usecount可还是0,只有真正拿到了才会++。另一个线程thread2在PageCache合并相邻的空闲的Span,如果相邻的就是thread1正准备拿的Span,你用_usecount==0判断是否被使用,那不就造成线程安全的问题了吗!!
所以不能用Span中的_usecount==0判断一个Span是否在使用。我们再给Span加一个_isuse成员来判断Span是否在使用,拿到CentralCache的Span就在使用,在PageCache的Span就没有在使用。
//Span管理一个跨度的大块内存
//管理以页为单位的大快内存
struct Span
{
PAGE_ID _pageid = 0;//管理一大块连续内存块的起始页的页号
size_t _n = 0;//管理几页
Span* _prev = nullptr;//带头双向循环链表前指针
Span* _next = nullptr;//带头双向循环链表后指针
size_t _usecount = 0;//记录Span对象中自由链表挂的内存块对象使用数量
void* _freelist = nullptr;//自由链表挂的是Span对象一块大连续内存块按照桶位置大小切分的一块块小的内存块
bool _isuse = false;//当前Span是否被使用
};
当CentralPage从PageCache获取到一个新的Span就已经被用了,增加一句代码。
// 获取一个非空的span
Span* CentralCache::GetOneSpan(Spanlist& list, size_t size)
{
//遍历对应桶中是否有span或者span中是否有内存对象
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freelist)
{
return it;
}
else
{
it = it->_next;
}
}
//当Central Cache对应桶下面没有span,那就往下一层Page Cache找,但是首先要先把对应桶锁释放
//如果再来一个线程是申请内存但是没有内存锁住也没问题,但是如果这个线程是来还内存的呢?
//锁住那不就还不了了,因此往下走之前先把桶锁释放
list._mtx.unlock();
//走到这里说明该桶中并没有span或者span中没有内存对象,Central Cache那就找到下一层Page Cache要一个span
//Page Cache 是一个按桶下标映射的哈希桶,第i号桶表示这个桶下面的span管理的都是i页page
//central找page要,关注的是要的span是管理k页的span,然后就去k号桶去要
//PageCache的锁是一把整锁,虽然也可以是桶锁但是消耗性能
//当PageCache对应桶没有span就往下面桶继续找,就涉及多个桶加锁解锁,
//因此我们在最外面直接把PageCache锁住,只加锁解锁一次即可
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//在CentralCache中的span是已经被使用的span
span->_isuse = true;//span被使用
PageCache::GetInstance()->_pageMtx.unlock();
//从PageCache拿上来的span是每一个线程独享的,因此切片时不需要加锁
//把从PageCache拿回的span切分成size大小的一个个内存块挂在自由链表,最后再把span挂在对应的桶中
//根据页号找到span管理的一大块内存的起始地址
//地址用char* 方便下面地址++
char* start = (char*)(span->_pageid << PAGE_SHIFT);
//计算这块内存有多大
size_t bytes = span->_n << PAGE_SHIFT;
//结尾地址
char* end = start + bytes;
//使用尾插法将大内存变成大小为size的一快快小内存挂在自由链表上
span->_freelist = start;
start += size;
void* tail = span->_freelist;
while (start < end)
{
NextObj(tail) = start;
tail = start;
start += size;
}
//自由链表最后为nullptr
NextObj(tail) = nullptr;
//切好span以后,需要把span挂到桶里面去的时候,再加锁
list._mtx.lock();
//再把span挂在对应桶
list.PushFront(span);
return span;
}
现在先别急着就去unordered_map去找在PageCache中相邻的Span,你现在肯定也找不到,还记得unordered_map目前记录的是什么吗?它当前只记录了在CentralCache中的Span管理的页号与Span的对应关系,也就是已经使用的Span的页号和Span的关系,根本就没有记录在PageCache中Span管理的页号SPan对应的关系。因此还要在修改一下代码。
在PageCache的Span,只需要记录它管理的首页和尾页与Span的映射关系就行了。合并我们就找相邻Span的首页和尾页就找到对应的Span了。对于在CentralCache中Span因为要被切分成一块块小内存然后才给ThreadCache用,当小内存块回来的时候,可能是这个Span中任意一页的小内块,然后/8KB,找到页号,在找到对应的SPan。因此要把Span中每一页和Span对应关系都保存。
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁
// 先检查第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;
_idSpanMap.set(Ispan->_pageid, Ispan);
_idSpanMap.set(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;
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);
}
准备工作做完了,接下来就是CentralPage还回来一个Span,看相邻的Span是否能合并,解决内存碎片的问题。
// 释放空闲span回到Pagecache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 如何知道相邻的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;
// 这里可能有疑问,那遗留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())
{
break;
}
Span* nextSpan = ret->second;
// 后面相邻页的span在使用,不合并了
if (nextSpan->_isuse == true)
{
break;
}
// 合并出超过128页的span没办法挂在桶下,不合并了
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_n += nextSpan->_n;
_spanlists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
//合并好的span挂在对应桶下
_spanlists[span->_n].PushFront(span);
span->_isuse = false;
//重新映射在PageCace的Span的首页和尾页与Span映射关系
_idSpanMap[span->_pageid] = span;
_idSpanMap[span->_pageid + span->_n - 1] = span;
}
关于释放内存就到这里了,下面我们在对细节进行优化。
2. 申请和释放剩余补充
当要的内存小于256KB,每个线程通过TLS找到自己的ThreadCache要,如果没有就找CentralCache要,在没有就找PageCache要,在没有就找堆去要。
之前我们一直没谈的是大于256KB怎么办?
其实这里分为分成两种情况,256KB/8KB=32Page,PageCache最大的桶是128Page。因此就有 大于32Page 小于等于128Page,找PageCache要。大于128Page直接找堆要。
不管是找PageCache要还是找堆要,都是按页为单位对齐的。
//Common.h
class SizeClass
{
//...
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
return ((bytes + alignNum - 1) & ~(alignNum - 1));
}
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else//找PageCache或者堆按页为单位对齐
{
return _RoundUp(size, 1 << PAGE_SHIFT);
}
return -1;
}
};
//ConcurrentAlloc.h
static void* ConcurrentAlloc(size_t size)
{
//大于256KB/8KB=13page 小于等于128page 找PageCache要
//大于128page找堆要
if (size > MAX_BYTES)
{
//找PageCache或者堆要都是以页为单位对齐
size_t alignnum = SizeClass::RoundUp(size);
//在PageCache那个桶
size_t kpage = alignnum >> PAGE_SHIFT;
//在PageCache内部处理找是否找堆要
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);
//这里不需要记录这个span已经被使用,因为还得时候合并的这个span相邻页的span看是否在使用
PageCache::GetInstance()->_pageMtx.unlock();
//得到地址返回
void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
return ptr;
}
else//小于256KB通过三层缓存要
{
//通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
return pTLSThreadCache->Allocate(size);
}
}
这里在把找PageCache要一个Span处理一下
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0);
//大于 128page找堆要
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
//申请一个span对象管理这块内存,释放内存的时候要对应的span
Span* span = new Span;
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;
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;
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);
}
释放也是一样,小于256KB的通过TLS找自己的ThreadCache释放,大于32Page小于等于128Page还给PageCache,大于128Page还直接返给堆。
static void ConcurrentFree(void* ptr,size_t size)
{
//大于32page小于等于128page直接还给PageCache,
//大于128page直接还给堆
if (size > MAX_BYTES)
{
//根据地址找转换为对应的页号,通过页号找到对应的span,在找到对应内存大小
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}