C++项目——高并发内存池(4)--page cache

文章介绍了PageCache的内存申请设计,包括其哈希桶结构和spanList的组织方式。在锁的设计上,由于需要遍历寻找并分割span,因此采用了大锁来确保线程安全。当内存返回时,PageCache会进行合并整理,减少内存碎片。CentralCache作为单例模式,负责从PageCache获取和分配内存。GetOneSpan函数的实现涉及到遍历链表和向PageCache申请内存的过程。
摘要由CSDN通过智能技术生成


1.page cache介绍

1.1 申请内存设计

page cache本质也是哈希桶结构,挂着的也是spanList,但映射关系和之前二者就不同了,设计共有128个桶,桶下面挂着与桶编号有关的span数,0号桶下面挂着的是1个1个的page span,3号桶下面挂着的是3个3个的page span...

当程序运行起来时,page cache桶结构全是空,它会用一定的方法向系统申请128页page span,挂在自由链表中。当central cache向page cache申请内存时,page cache 先检查对应位置有没有span,如果没有就会向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找,假设在第10页找到了一个span,则将其分裂为一个4页page span和一个6页page span

1.2锁的设计

桶锁可以解决线程同时访问一个span的问题,但是拿走页之后pagecache会分割页,然后将不用的挂起来,桶锁不能锁住这一临界区,是不安全的,所以只能采用一把大锁,锁住pagecache对象

而且,central cache可以使用桶锁的原因是会去指定的地方寻找span,只需要访问该桶,但是对于page cache是需要由page span从小到大遍历,去寻找一个非空,然后将其分割。如果使用桶锁,每进入一个桶就要发生加锁和解锁过程,增多不必要的消耗。

 1.3 合并整理设计

 如果central cache中span usecount等于0,说明切分给thread cache小块的内存都回来了,则central cache把这个span还给page cache,page cache通过页号,查看前后的相邻页是否空闲,是就合并,合并成更大的页,解决内存碎片问题。

2. PageCache的实现

声明

//因为所有的线程都从一个central cache中获取值 所以我们将它设计为单例模式
class CentralCache
{
public:
	static CentralCache* GetInstance()
	{
		return &_sInst;
	}
	//从中心缓冲中获取一定数量的对象给thread cache
	size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);

	// 获取一个非空的span
	Span* GetOneSpan(SpanList& list, size_t byte_size);


private:
	CentralCache()
	{}
	CentralCache(const CentralCache& abc) = delete;
private:
	static CentralCache _sInst;//记得类外初始化
	SpanList _spanLists[NFREELISTS]; 
};

 2.1 Span* CentralCache::GetOneSpan()

 在上篇博文中,我们留下了一个问题 就是GetOneSpan()的实现,现在既然有了CentralCache的定义,所以这里先构思一下这个函数的实现。

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)

首先,寻找一个非空的Span,有两种可能

一、从 _spanLists中遍历,找到一个span a,如果a._freeList不为空就把a返回

二、_spanLists没有空闲的span了,只能找page cache要。那要多少呢?不能每次都一次要一个吧?

2.1.1 _spanLists的遍历

SpanList类是一个双向循环的带头链表,要想遍历整个链表就需要确定起始位置和终止位置,所以在SpanList类中需要新增两个接口,用于返回起始位置和结束位置。

class SpanList
{ 
public:
	//...
	Span* Begin()
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}

    //后续会用到
    void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}
	Span* PopFront()
	{
		Span* Pop = _head->_next;
		Erase(Pop);
		return Pop;
	}
	//...
private:
	Span* _head=nullptr;	
};

2.1.2 SizeClass::NumMovePage()

设计一个期望拿到页数的函数。

class SizeClass
{
pblic:
    //写过的略 ..
    //计算一次从page cache获取几个页
	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num * size;
		npage >>= PAGE_SHIFT;//右移13位 相当于/=8k
		if (npage == 0) npage = 1;
		return npage;
	}
};

 2.1.3 函数实现

Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
	//先查看当前的 _spanLists看看还有没有未满的span
	Span* it = list.Begin();
	while (it != list.End())
	{
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else
		{
			it = it->_next;
		}
	}
	//现在只能向page cache中申请了
	/*
	* 桶锁解不解呢? 对于申请来讲,假如一号线程在1号桶内没有发现空余的span跑去page cache申请
	* 那二号线程在访问1号桶的时候,无论解锁锁与不解锁他都不能往下执行,均可。
	* 但是对于释放来讲,他只是将回收的内存挂在1号桶,如果不解锁,这个过程需要等到1号线程申请
	* 完毕后才能进行,程序的效率会下降。
	*/
	list._mtx.unlock();

	PageCache::GetInstance()->_pageMtx.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
	PageCache::GetInstance()->_pageMtx.unlock();
	
	//拿到了 开始切分 不需要加锁 因为还没有挂到桶上 其他线程拿不到这个span
	char* start = (char*)(span->_pageId << PAGE_SHIFT);//意思是用 第几个页*页的大小 算出来起始位置
	char* end = start + (span->_pageId << PAGE_SHIFT);
	span->_freeList = start;
	//采用尾插的方式可以保证切好的内存空间是连续的
	start += size;
	void* tail = span->_freeList;
	while (start < end)
	{
		NextObj(tail) = start;
		tail = start;
		start += size;
	}

    //记得尾指针置空
    Nextobj(tail)=nullptr;
	//将切好的span 挂起来 在临界区内 需要加锁 不能一边申请你一边往里面放
	list._mtx.lock();
	list.PushFront(span);
	return span;
}

2.2 Span* PageCache::NewSpan( )

2.2.1 向堆申请一个128页的空间

在一开始时,page cache上所有的桶都是空,需要从堆申请一个大块的内存。

#ifdef _WIN32
#include <windows.h>
#else
// ...
#endif

// 直接去堆上按页申请空间
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
	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;
}

 因为该函数返回的是一个void*的指针,我们怎么把该指针和页号联系在一起呢?

4G的内存空间本质上就是一个一个连续的页构成的,所以16进制的地址本质上也是从0开始的,递增的数字。所以将地址转为整数然后除以一个页的大小,得到的就是页的编号!

2.2.2 函数实现

Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0 && k < NPAGES);
	//在page cache中 页号和哈希桶的下标是对应的
	if (!_spanLists[k].Empty())
	{
		return _spanLists[k].PopFront();
	}
	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);
			return kSpan;
		}
	}
	//走到这里 说明后面没有大页的span了
	Span* bigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT; //4G的内存空间 其实就是从0开始 一个一个的页构成的
	bigSpan->_n = NPAGES - 1;
	_spanLists[bigSpan->_n].PushFront(bigSpan);
	//现在要切分bigSpan了 但是按理来说不在项目里面写重复的代码
	//所以采用递归调用
	return NewSpan(k);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值