点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
1. thread cache
thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的。
我们每次申请内存的大小是不确定的,有可能有1个字节,也可能有100KB,但并不是每个字节都需要有一个对应的自由链表,256kb=262144byte,那一个threadcache就有20多万个自由链表,这样太占内存了。因此我们按照一定的对齐规则,牺牲一点内存。比如说小于等于8byte给它8byte,大于8小于16字节给他16字节,虽然有一些浪费,但是总体浪费10%左右。
10%是怎么算出来的呢?
256kb/8=32678,也有3万多个自由链表,还是太大了。所以不能都按照8byte对齐。因此有了下面的对齐规则。
整体控制在最多10%左右的内碎片浪费
[1,128] 8byte对齐
[128+1,1024] 16byte对齐
[1024+1,8*1024] 128byte对齐
[8*1024+1,64*1024] 1024byte对齐
[64*1024+1,256*1024] 8*1024byte对齐
按8byte对齐的我们就不算了,算下面的,128+1对齐16字节,还要在加上15字节,一共是144,我们多给了15个字节,浪费大约10%左右
1024 + 1对齐128字节,多给个127,也差不多浪费10%左右
越往下分母越大,所以总体浪费10%左右,是可以接受的。再说了即使这次你没用那么多,你还回来之后,给别人要那么多内存的去用,所以总体还是可以的。
在考虑一个问题,那ThreadCache这个哈希桶总共多大呢?
整体控制在最多10%左右的内碎片浪费
[1,128] 8byte对齐 freelist[0,16)
[128+1,1024] 16byte对齐 freelist[16,72)
[1024+1,8*1024] 128byte对齐 freelist[72,128)
[8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
[64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
按8byte对齐,128/8 = 16个桶
按16byte对齐,1024其中有128已经按照8byte对齐,1024-128=896,按照16字节对齐,896/16 = 56 个桶
按128byte对齐,(8*1024-1024)/128 = 56 个桶
。。。最终ThreadCache哈希桶总共有208个桶。
//Common.h 公共的都用这里
#include<iostream>
#include<assert.h>
using std::cout;
using std::cin;
using std::endl;
// 小于等于MAX_BYTES,就找thread cache申请
static const size_t MAX_BYTES = 256 * 1024;
// thread cache 哈希桶的表大小
static const size_t NFREELIST = 208;
//获取当前内存块前4/8byte
static void*& NextObj(void* obj)
{
return *(void**)obj;
}
// 管理切分好的⼩对象的⾃由链表
class Freelist
{
public:
//释放内存
void Push(void* obj)
{
//头插
assert(obj);
NextObj(obj) = _freelist;
_freelist = obj;
}
//申请内存
void* Pop()
{
//头删
assert(_freelist);
void* obj = _freelist;
_freelist = NextObj(obj);
return obj;
}
//链表是否为空
bool IsEmpty()
{
return _freelist == nullptr;
}
private:
void* _freelist = nullptr;
};
//ThreadCache.h
// thread cache本质是由⼀个哈希映射的对象⾃由链表构成
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
// 从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
private:
Freelist _freelists[NFREELIST];
};
那拿到一个要申请内存的大小如何去将内存对齐呢?还有如何找到在自己的ThreadCache的那一个桶去拿呢?
针对这个问题我们写一个公共SizeClass类,关于内存对齐和映射ThreadCache那个一个桶可以代入数自己算。
//Common.h 公共的都用这里
// 计算对象大小的对齐映射规则
class SizeClass
{
public:
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [128+1,1024] 16byte对齐 freelist[16,72)
// [1024+1,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
// 1 (1+8-1)&~(7)=8
// 7 (7+8-1)&~(7)=8
// 8 (8+8-1)&~(7)=8
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
{
assert(false);
return -1;
}
}
// 1 + 7 8
// 2 9
// ...
// 8 15
// 9 + 7 16
// 10
// ...
// 16 23
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
// 计算映射的哪一个自由链表桶
static inline size_t Index(size_t bytes)
{
// 每个区间有多少个桶
static int group_array[4] = { 16,56,56,56 };
if (bytes <= 128)
{
return _Index(bytes, 3);
}
else if (bytes <= 1024)
{
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 8 * 1024)
{
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024)
{
return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
}
else if (bytes <= 256 * 1024)
{
return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else
{
assert(0);
return -1;
}
}
};
申请内存:
- 当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自由链表下标i。
- 如果自由链表_freelists[i]中有对象,则直接Pop一个内存对象返回。
- 如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到对应的自由链表并返回一个对象。
#include"ThreadCache.h"
#include"CentralCache.h"
// 从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
return nullptr
}
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_BYTES);
size_t alignnum = SizeClass::RoundUp(size);
size_t index = SizeClass::Index(size);
if (!_freelists[index].IsEmpty())
{
return _freelists[index].Pop();
}
return FetchFromCentralCache(index, alignnum);
}
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
// 找对映射的自由链表桶,对象插入进入
size_t index = SizeClass::Index(size);
_freelists[index].Push(ptr);
}
前面不是说每个线程都有自己的ThreadCache吗,但是那个ThreadCache是那个线程的呢?一个线程什么时候创建ThreadCache呢?一个进程大部分资源是所有线程共享的,线程同时去创建ThreadCache是不是就有加锁了问题,如何解决加锁的问题?不想加锁。
这里我们使用Thread Local Storage(线程局部存储)TLS
线程局部存储(TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,每个线程都有一份自己的,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。
//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);
private:
Freelist _freelists[NFREELIST];
};
// TLS thread local storage 线程局部存储
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
//ConcurrentAlloc.h
#include"ThreadCache.h"
#include"PageCache.h"
static void* ConcurrentAlloc(size_t size)
{
//通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
return pTLSThreadCache->Allocate(size);
}
当ThreadCache对应桶下没有空闲内存块了,那就去找CentralCache要。
2. central cache
central cache也是一个哈希桶结构,他的哈希桶的映射关系跟thread cache是⼀样的。不同的是他的每个哈希桶位置挂是Spanlist链表结构,并且每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。
CentralCache也是一个哈希桶,它的映射关系和ThreadCache一样,所有线程共享一个CentralCache因此需要加锁,但是这个锁是一个桶锁,并不是直接把CentralCache整个锁起来。比如说thread1和thread2都要16KB的内存块,但是自己的ThreadCache对应桶下没有,只能找CentralCache去要,由于都是去16KB大小的桶去要因此要竞争同一个桶锁,如果thread1要的是8KB,thread2要的是16KB,由于去的是CentralCache不同的桶,虽然都会加桶锁,但不是同一把锁,因此不存在锁的竞争。
ThreadCache的桶挂的是一个个内存块对象,CentralCace的桶挂的是一个个Span对象,是以页为单位的大快内存,并且每个Span管理的大块内存都按照映射关系切成了一个个小内存块对象挂在span的自由链表中。
以页为单位的大内存管理span的定义,一页4KB/8KB大小,这里我们一页就按8KB。
//Common.h
// 页编号类型,32位下是2^32/2^13=2^19,一共有2^19页, size_t可以满足
// 64位下是2^64/2^8=2^51页,size_t满足不了
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#else // linux环境下
//...
#endif
//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对象一块大连续内存块按照桶位置大小切分的一块块小的内存块
};
Span我们用带头双向循环链表来管理,插入好说,但是要在一个链表中删除Span,需要找到它的前一个,因此用双向,但又涉及到头插头删,所以用带头双向循环链表来管理Span比较方便。
//Common.h
//带头双向循环链表
class Spanlist
{
public:
Spanlist()
{
_head = new Span;
_head->_prev = _head;
_head->_next = _head;
}
void PushFront(Span* span)
{
Insert(Begin(), span);
}
Span* PopFront()
{
Span* front = Begin();
Erase(front);
return front;
}
bool Empty()
{
return _head->_next == _head;
}
//头插,尾插,任意位置插入
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
//头删,尾删,任意位置删除
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
//不是真的把Span对象删除,而是要还给Page Cache,外面会拿这个Span
}
private:
Span* _head;
public:
std::mutex _mtx;//每个桶都有自己的桶锁
};
//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);
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
CentralCache& operator=(const CentralCache&) = delete;
private:
Spanlist _spanlists[NFREELIST];
static CentralCache _sInst;
};
申请内存:
- 当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对象的数量使用了类似网络tcp协议拥塞控制的慢开始算法;central cache也有一个哈希映射的spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不过这里使用的是一个桶锁,尽可能提高效率。
- central cache映射的spanlist中所有span的都没有内存以后,则需要向page cache申请一个新的span对象,拿到span以后将span管理的内存按照桶映射大小切成一块块小内存作为自由链表链接到一起。在把span挂到对应的桶中,然后从span中取对象给thread cache。
- central cache的中挂的span中use_count记录分配了多少个对象出去,分配一个对象给threadcache,就++use_count
当ThreadCache对应桶没有内存块找CentralCache要,一次要一批量,一批量多少合适?少了不够,多了用不完浪费。因此使用慢启动算法,这里我们还需要有要对应内存的一个阈值。取阈值我们还放在SizeClass这个公共类里。
//Common.h
class SizeClass
{
//...
// 一次thread cache从中心缓存获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size > 0);
// [2, 512],一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
size_t num = MAX_BYTES / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
};
然后ThreadCache每个桶都要增加一个成员,记录下一次找CentralCache要一批量内存块的个数。刚开始要1个,每次+1,直到超过阈值,就按阈值找CentralCache要。
//Common.h
class Freelist
{
public:
//...
//获取当前桶获取下一批内存块的个数
size_t& GetMaxSize()
{
return _maxSize;
}
//一次挂一批
void PushRange(void* start,void* end)
{
NextObj(end) = _freelist;
_freelist = start;
}
private:
void* _freelist = nullptr;
size_t _maxSize = 1;//记录当前桶下一次找ContraltCache一次要一批量是多少个内存对象
};
//ThreadCache.cpp
#include"ThreadCache.h"
#include"CentralCache.h"
// 从中心缓存获取对象
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//一次从中心缓存获取一批的数量多少合适? 少了不够,多了浪费,因此有慢启动算法
//NumMoveSize获取申请大小为size内存块对象的阈值,但是刚开始就申请到阈值可能用不完就浪费了
//因此每个桶从1个开始申请,进行慢启动增加,下次申请就增加1个,如果超过阈值,就按阈值.
size_t batchnum = min(_freelists[index].GetMaxSize(),SizeClass::NumMoveSize(size));
if (batchnum == _freelists[index].GetMaxSize())
{
_freelists[index].GetMaxSize() += 1;
}
//记录从中心缓存申请到一批内存对象的起始地址和结尾地址
void* start = nullptr;
void* end = nullptr;
//从一个Span对象中获得内存对象,但Span内的内存对象数量可能不满足batchnum
size_t actualnum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchnum, size);
assert(actualnum > 0);
//从Span只获得了一个内存块对象,直接不用在挂到ThreadCache桶上,直接返回即可
if (actualnum == 1)
{
assert(start == end);
return start;
}
else//获得多个,将第一个返回,剩下挂在ThreadCache对应桶上
{
_freelists[index].PushRange(NextObj(start), end);
return start;
}
}
//CentralCache.hpp
#include"CentralCache.h"
#include"PageCache.h"
//静态对象
CentralCache CentralCache::_sInst;//定义放在这里防止被多个.cpp重复引用
// 获取一个非空的span
Span* CentralCache::GetOneSpan(Spanlist& list, size_t size)
{
return nullptr;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
//多个线程竞争问题,先加桶锁
size_t index = SizeClass::Index(size);
_spanlists[index]._mtx.lock();
Span* span = GetOneSpan(_spanlists[index], size);
assert(span);
assert(span->_freelist);
// 从span中获取batchNum个对象
// 如果不够batchNum个,有多少拿多少
start = end = span->_freelist;
size_t i = 0;
size_t actualnum = 1;//实际返回多少个内存对象
while (i < batchNum - 1 && NextObj(end))//end所指向的下一个内存快对象不能为nullptr,不然会有空解引用的问题
{
end = NextObj(end);
++i;
++actualnum;
}
//当前span被切走多少内存对象
span->_usecount += actualnum;
//连接end后面的内存块对象
span->_freelist = NextObj(end);
//将start到end这段链表和这个span中的自由链表断开
NextObj(end) = nullptr;
_spanlists[index]._mtx.unlock();
return actualnum;
}
3. PageCache
central cache和page cache 的核心结构都是spanlist的哈希桶,不过page cache的映射规则和central cahce不一样,它是按照下标桶号映射的,也就是说第i号桶下面挂的span是管理i页的内存。并且虽然page cache下面挂的也是span,但是它的span不像central cache那样把span管理的大块内存块按照桶位置映射切成一块块小内存块。并且contral cache找page cache要内存,关注的是要管理几页的span。
page cache桶大小我们给128个,也就是说一个span最多管理128page,128page*8kb=1024kb=1M,即使从central cache找page cache要的是小内存是256kb的span,阈值是要2个,那么在128page的span,1M/256KB=4个,也就是可以给你4个256kb的span。可以满足上面的要求。
PageCache也是所有线程共享的,因此也需要加锁,不过它这把锁是整给把PageCache锁起来的。虽然也可以继续用桶锁,但是可能PageCache当前桶并没有Span,然后继续往下面桶找,涉及到桶多次加锁解锁的情况导致效率低,不如直接用一把锁将PageCache整体锁住。
//Common.h
// page cache 管理span list哈希桶的表大小
//128*8KB=1M/256KB=4,最大桶下面一个span可以分成4个256KB。1下标对应1号桶,所有多一个0号桶
static const size_t NPAGES = 129;
// 页大小转换偏移, 即一页定义为2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;
#include"Common.h"
#include"ObjectPool.h"
#include"PageMap.h"
//所有线程共享一个PageCache,单例模式:饿汉(静态对象)
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//从PageCache第k号桶中获取一个span对象
Span* NewSpan(size_t k);
private:
PageCache()
{}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
private:
Spanlist _spanlists[NPAGES];
public:
std::mutex _pageMtx; //整个锁
};
申请内存:
- 当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page span分裂为一个4页page span和一个6页page span。
- 如果找到_spanlist[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。
- 需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存
先把找CentralPage对应桶要一个Span写完
//Common.h
// 计算对象大小的对齐映射规则
class SizeClass
{
public:
//...
// 计算一次向系统获取的是管理几page的span
// 单个对象 8byte
// ...
// 单个对象 256KB
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;
//至少给的span对象管理1page
if (npage == 0)
return 1;
return npage;
}
};
//带头双向循环链表
class Spanlist
{
public:
//...
Span* Begin()
{
return _head->_next;
}
Span* End()
{
return _head;
}
private:
Span* _head;
public:
std::mutex _mtx;//每个桶都有自己的桶锁
};
//ContralCache.cpp
// 获取一个非空的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;
}
}
//走到这里说明该桶中并没有span或者span中没有内存对象,Central Cache那就找到下一层Page Cache要一个span
//Page Cache 是一个按桶下标映射的哈希桶,第i号桶表示这个桶下面的span管理的都是i页page
//central找page要,关注的是要的span是管理k页的span,然后就去k号桶去要
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//把从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挂在对应桶
list.PushFront(span);
return span;
}
CentarlPage对应桶没有Span或者SPan中没有空闲的内存块就找PageCache对应桶要一个Span,如果没有就往下面桶要,下面有就切分成两个Span,一直到最后一个桶还没有,PageCache就去找堆要一个128page的Span,然后继续重复上述过程,因此我们这里写成递归。但是要考虑到一个问题,PageCache也是所有线程共享的,因此需要加锁。如果在在递归里面用互斥锁就有问题了,除非你换递归互斥锁,还有一种方法就是在调用这个函数之前加锁,调用完之后解锁。
//Common.h
// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
//申请kpage页,每页8kb
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
//PageCache.cpp
//从PageCache第k号桶中获取一个span对象
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//这里加锁递归会有死锁问题,除非用递归互斥锁,还有在外面调用这个函数之前加锁
// 先检查第k个桶里面有没有span
if (!_spanlists[k].Empty())
{
return _spanlists[k].PopFront();
}
//走到这里第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);
//将管理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);
}
这里补充说明一下如何将地址转换出起始页号,地址/8k就得到起始页号了。
那在考虑这样一个问题,从CentralCache到PageCache之前,对应的桶锁要不要解除?其实是应该解除的。这是一个多线程并发的场景,如果再来一个线程也在对应桶中申请内存,那目前没有内存,锁住是没问题的,但是万一这个下次是来还内存的呢?锁住不就每办法还了吗!
//CentralCache.cpp
// 获取一个非空的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));
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;
}
这里在补充说明一下如何通过页号找到Span管理内存的起始地址。拿到一个Span得到起始页号,一页8KB,页号*8KB 等价于 页号 << (1<<13),得到的就是这块内存的起始地址。
申请内存到现在差不多了,不过上面还有很多细节还没有处理,下面我们在一一补充!