006 高并发内存池_PageCache设计

​🌈个人主页:Fan_558
🔥 系列专栏:高并发内存池
🌹关注我💪🏻带你学更多知识

在这里插入图片描述

文章目录

  • 前言
    • 文章重点
    • 一、回顾PageCache页缓存结构
    • 二、PageCache结构设计
    • 三、完善申请内存函数
    • 小结

前言

本文将会带你走进高并发内存池PageCache页缓存的设计

文章重点

在此模块中,我们将要完成以下任务
1、回顾PageCache页缓存结构
2、PageCache结构设计
3、完善GetoneSpan获取一个非空的span与在PageCache中获取一个n页的span

一、回顾PageCache页缓存结构

前文提到这里我们就最大挂128页的span,为了让桶号与页号对应起来,我们可以将第0号桶空出来不用,因此我们需要将哈希桶的个数设置为129。
线程申请单个对象最大是256KB,而128页可以被切成4个256KB的对象,因此是足够的。当然,如果你想在page
cache中挂更大的span也是可以的,根据具体的需求进行设置就行了

在这里插入图片描述

当线程向ThreadCache申请内存对象的时候,ThreadCache没有就要去CentralCache要,CentralCache没有就要去PageCache要,当PageCache也没有的话,只能去堆上申请,在定长池一篇文章中,我们提到过向系统申请内存的接口,并且封装了它
当PageCache中也没有内存时,此时需要向系统(堆)申请一个128Page大小的内存span,将kspan=128传入作为参数传入

#ifdef _WIN32
#include <Windows.h>
#else
#endif

PageCache PageCache::_sInst;

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;
}

此时我们再来观察span的内部结构,其中_PageId是所申请大块内存起始页的页号,这个页号是与从堆上被分配的内存的起始地址有关系的,我们假设一页内存的大小是8K,那么将这个起始地址➗8K就是它的起始页号,再来观察传给VirtualAlloc的参数
kpage << 13,也就是用页数✖8K(2^13)作为所申请的内存大小

//管理以页为单位的大块内存
struct Span
{
	//给缺省值,可以不用提供构造函数
	PAGE_ID _pageId = 0;        //大块内存起始页的页号(从堆上分配内存的起始地址
	size_t _n = 0;              //页的数量

	Span* _next = nullptr;      //双链表结构
	Span* _prev = nullptr;

	size_t _useCount = 0;       //切好的小块内存,被分配给thread cache的计数
	void* _freeList = nullptr;  //切好的小块内存的自由链表
};

二、PageCache结构设计

PageCache在整个进程中也是只能存在一个的,由此我们也将其设置为单例模式

//单例模式(饿汉
class PageCache
{
public:
	//提供一个全局访问点
	static PageCache* GetInstance()
	{
		return &_sInst;
	}
private:
	SpanList _spanLists[NPAGES];
	std::mutex _pageMtx; //大锁
private:
	PageCache() //构造函数私有
	{}
	PageCache(const PageCache&) = delete; //防拷贝

	static PageCache _sInst;
};

当程序一运行,该对象就被创建

PageCache PageCache::_sInst;

三、完善申请内存函数

一、GetoneSpan模块

1、先在CentralCache对应桶中遍历span,如果不为空就返回(上文结尾提到)

2、这里需要注意:当CentralCache中span为空的时候,此时我们需要向PageCache申请,在此之前
需要先将CentralCache上对应的桶锁给解开,因为当一个线程持续向下申请,threadcache->centralcache->pagecache,此时直至申请到了pagecache,然而centralcache是有桶锁的,是需要线程一走后进行解锁的
不然当线程二申请,虽然申请不到,因为本就没有内存,但是如果线程二是释放呢,那么影响就很大了
所以就需要向pagecache申请之前将锁解开,这样如果其它线程释放内存回来,不会阻塞。

3、对于PageCache的结构我们是需要给一把大锁的,直接在申请PageCache的span函数加锁解锁即可
疑问:为什么不像CentralCache一样设置对每一个桶设置一个桶锁呢?
首先PageCache不像centralcache一样匹配到哪个桶就去哪个桶中申请,没有就向pagecache申请,各个桶之间不会有过多的交集
但是PageCache不一样,如果第k号桶没有,会到k-128的桶全部遍历一遍,不是说桶锁不行,而是频繁的加锁解锁唤醒睡眠,效率变低

(在NewSpan在PageCache中获取一个n页的span函数模块会讲)

	PageCache::GetInstance()->_pageMutex.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(AlignNum));
	PageCache::GetInstance()->_pageMutex.unlock();

Newspan模块申请一个n页的span先保留,我们先将此模块的代码逻辑完善

4、将一个从PageCache申请的大块内存切分成由一个span指向的自由链表

在这里插入图片描述

5、最后将一大块从PageCache申请到的内存且切好的自由链表挂在CentralCache对应的桶中

根据需求修改SpanList结构

//带头双向循环链表
class SpanList
{
public:
	//初始化双向链表
	SpanList()
	{
		//初始化头节点
		_head = new Span;
		_head->_next = _head;
		_head->_prev = _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);
		Span* prev = pos->_prev;
		Span* next = pos->_next;
		prev->_next = next;
		next->_prev = prev;
		//不需要真正delete该pos处的span,可能需要还给pagecache
	}
	Span* Begin()
	{
		return _head->_next;
	}
	Span* End()
	{
		return _head;
	}
	//头插
	void PushFront(Span* span)
	{
		Insert(Begin(), span);
	}
	//头删
	Span* PopFront()
	{
		Span* front = _head->_next;
		Erase(front);
		return front;
	}
	bool Empty()
	{
		return _head->_next == _head;
	}
private:
	Span* _head;
public:
	std::mutex _mtx; //桶锁
};

获取一个非空的span:整体代码

//获取一个非空的span
Span* CentralCache::GetoneSpan(SpanList& list, size_t AlignNum)
{
	//从list中取出一个非空的span,遍历
	Span* it = list.Begin();
	while (it != list.End())
	{
		//存在非空的span就返回
		if (it->_freeList != nullptr)
		{
			return it;
		}
		else it = it->_next;
	}
	//将centralcache的桶锁解开,这样如果其它线程释放内存对象回来,就不会阻塞了
	list._mtx.unlock();
	//没有非空的span,向PageCache中申请
	PageCache::GetInstance()->_pageMutex.lock();
	Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(AlignNum));
	PageCache::GetInstance()->_pageMutex.unlock();
	//这里不需要立刻立刻将该线程的桶锁给续上呢,不用,因为只有此线程是拿到这个span的,其它线程没有
	//计算大块内存的起始地址以及字节数
	char* start = (char*)(span->_pageId << PAGE_SHIFT);		//起始地址
	size_t Bytes = span->_n << PAGE_SHIFT;
	char* end = start + Bytes;
	//将一大块从PageCache中申请的内存切分成由一个span指向的自由链表
	span->_freeList = start;
	start += AlignNum;
	void* tail = span->_freeList;
	while (start < end)
	{
		FreeList::NextObj(tail) = start;
		tail = start;
		start += AlignNum;
	}
	list._mtx.lock();
	//将span挂到桶里面去
	list.PushFront(span);
	return span;
}

二、NewSpan模块:
将转化好的需要申请的页数传参给NewSpan

Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(AlignNum));

将申请的字节数转化为页数,不足一页给一页

	static size_t NumMovePage(size_t size)
	{
		size_t num = NumMoveSize(size);
		size_t npage = num * size;

		npage >>= PAGE_SHIFT;
		if (npage == 0)
			npage = 1;

		return npage;
	}

NewsSpan:从PageCache中获取一个n页的span

1、首先查看对应页数(K)的桶中是否存在span,如果存在直接返回,若不存在,遍历整个PageCache结构中K之后的桶中是否存在非空的span,如果有直接返回,如果都没有,只能向堆进行申请一个128Page大小的内存span

2、然后将申请的内存起始地址转换成页号_pageId,_n赋值成页数的大小即128,最后将128Page大小的span挂到对应的桶中

	newBigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	newBigSpan->_n = NPAGES - 1;
	//在pagecache对应的桶中插入刚申请的内存span
	_spanLists[newBigSpan->_n].PushFront(newBigSpan);

3、最后复用自己,这时遍历K以后桶的时候,遍历到第128个桶时,就获取到了span,然后将128-K页重新找对应的桶挂起来,返回K页内存大小的span

	//复用自己,重新进行切分
	return NewSpan(K);
//在pagecache中获取一个n页的span
Span* PageCache::NewSpan(size_t K)
{
	std::cout << K << std::endl;
	assert(K > 0 && K < NPAGES);
	//检查pagecache第K个桶是否有span
	if (!_spanLists[K].Empty())
	{
		return _spanLists[K].PopFront();
	}
	//查看第K个桶的后面的桶是否有span(K+1:跳过当前没有span的桶)
	for (size_t i = K + 1; i < NPAGES; i++)
	{
		if (!_spanLists[i].Empty())
		{
			//切分span
			Span* Nspan = _spanLists[i].PopFront();
			Span* Kspan = new Span;
			//起始页号
			Kspan->_pageId = Nspan->_pageId;
			//页数
			Kspan->_n = K;
			Nspan->_pageId += K;
			Nspan->_n -= K;
			//将切分剩下的页缓存重新挂起来
			_spanLists[Nspan->_n].PushFront(Nspan);
			return Kspan;
		}
	}
	//其余桶为空,此时向(堆)系统申请一个128Page的内存块
	Span* newBigSpan = new Span;
	void* ptr = SystemAlloc(NPAGES - 1);
	newBigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
	newBigSpan->_n = NPAGES - 1;
	//在pagecache对应的桶中插入刚申请的内存span
	_spanLists[newBigSpan->_n].PushFront(newBigSpan);
	//复用自己,重新进行切分
	return NewSpan(K);
}

小结

今日的项目分享就到这里啦,三层申请内存的结构终于完成啦,下期预告:测试三层内存架构,欢迎交流学习~
如果本文存在疏漏或错误的地方,还请您能够指出!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/505396.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

聊一聊电子邮件?

电子邮件是什么&#xff1f; 电子邮件是一种基于客户/服务器架构的应用。功能是实现人与人之间的交流。直到现在&#xff0c;电子邮件依然是当前因特网 注意&#xff1a;基于客户/服务器方式和基于B/S架构不一样&#xff01;客户/服务器表示的范围更广&#xff0c;当基于客户…

Linux环境jdk、git、maven、MySQL和redis的安装和配置

这里整理了在Linux上如何安装和配置jdk、git、maven、MySQL和redis&#xff0c;主要用于在Linux上部署Java项目 文章中博主使用了一个叫FinalShell远程连接软件进行上传&#xff0c;如果没有类似的工具也可以直接通过yum命令行下载包 博主使用的Linux服务器为centos7&#xf…

2024最新版Android studio安装入门教程(非常详细)

目录 JDK安装与配置 一、下载JDK 二、JDK安装 三、JDK的环境配置 四、JDK的配置验证 Android studio安装 Android studio连接手机真机调试&#xff08;以华为鸿蒙为例&#xff09; 一、新建一个android项目 二、进入项目面板 三、配置Android Studio 四、安装手机驱…

【JavaSE】初识线程,线程与进程的区别

文章目录 ✍线程是什么&#xff1f;✍线程和进程的区别✍线程的创建1.继承 Thread 类2.实现Runnable接口3.匿名内部类4.匿名内部类创建 Runnable ⼦类对象5.lambda 表达式创建 Runnable ⼦类对象 ✍线程是什么&#xff1f; ⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按…

BigInteger的应用

这里写目录标题 例题BigInteger常用方法关于BigInteger初始化为nullcompareTo()方法 : 返回一个int型数据(1 大于; 0 等于 ; -1 小于) 例题 import java.math.BigInteger; import java.util.*; public class Main{public static void main(String[] args) {BigInteger n BigIn…

android 消息提醒

1.创建 MyBackgroundService.java 继承 Service public class MyBackgroundService extends Service {Overridepublic void onCreate() {super.onCreate();Log.i("业务服务", "开起业务服务");//调用服务后在页面手机上创建一个通知消息。if (android.os…

TS的基础

TS Typed JavaScript at Any Scale. 它强调了 TypeScript 的两个最重要的特性——类型系统、适用于任何规模。 我们知道&#xff0c;JavaScript 是一门非常灵活的编程语言&#xff0c; 它没有类型约束&#xff0c;一个变量可能初始化时是字符串&#xff0c;过一会儿又被赋值为…

如何用 C++ 在 10 行内写出八皇后?

在编程世界中&#xff0c;有时挑战在于以最简洁的方式表达复杂的逻辑。八皇后问题就是这样一道经典难题&#xff0c;它要求在88的棋盘上放置8个皇后&#xff0c;使得任意两个皇后之间都不能位于同一行、同一列或同一斜线上。虽然这个问题可以通过多种算法解决&#xff0c;包括递…

Scala介绍与环境搭建

Scala环境搭建与介绍 一、Scala环境搭建 1、环境准备与下载 2、验证Scala 3、IDEA新建项目&#xff0c;配置Scala&#xff0c;运行Hello world 二、Scala介绍 1、Scala 简介 2、Scala 概述 一、Scala环境搭建 1、环境准备与下载 JDK1.8 Java Downloads | Oracle 下载需求版本…

代码随想录|Day29|贪心04|860.柠檬水找零、406.根据身高重建队列、452.用最少数量的箭引爆气球

860.柠檬水找零 我们维护三种金额的数量&#xff1a;five&#xff0c;ten&#xff0c;twenty 有如下三种情况&#xff1a; 账单是5&#xff1a;five 1&#xff0c;无需找零账单是10&#xff1a;ten 1&#xff0c;找零一张5元&#xff08;five - 1&#xff09;账单是20&#x…

Eclipse EMF教程(上)

What every Eclipse developer should know about EMF 翻译自&#xff1a;https://eclipsesource.com/blogs/tutorials/emf-tutorial/ 本教程是对EMF的介绍&#xff0c;解释了EMF的基础知识。我们首先向您展示如何基于EMF构建一个非常简单的以数据为中心的应用程序&#xff0c…

MES_ENT_STD

生产执行系统&#xff08;企业标准版&#xff09;MES_ENT_STD ERP_ENT_STD_59438.ieqq.ent-CSDN博客 OAMS_ENT_STD-CSDN博客

分类预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积长短期记忆网络多输入分类预测

分类预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积长短期记忆网络多输入分类预测 目录 分类预测 | MATLAB实现BO-CNN-LSTM贝叶斯优化卷积长短期记忆网络多输入分类预测效果一览基本介绍模型搭建程序设计参考资料 效果一览 基本介绍 MATLAB实现BO-CNN-LSTM贝叶斯优化卷积长短期记…

主干网络篇 | 利用RT-DETR模型主干HGNet去替换YOLOv8的主干

前言:Hello大家好,我是小哥谈。众所周知,实时目标检测(Real-Time Object Detection)一直被YOLO系列检测器统治着,YOLO版本更是炒到了v9,前段时间百度飞桨的PaddleDetection团队发布了一个名为RT-DETR的检测器,宣告其推翻了YOLO对实时检测领域统治。论文标题很直接:《D…

WPF上使用MaterialDesign框架---下载与配置

一、介绍&#xff1a; Material Design语言的一些重要功能包括 系统字体Roboto的升级版本 &#xff0c;同时颜色更鲜艳&#xff0c;动画效果更突出。杜拉特还简要谈到了新框架的一些变化。谷歌的想法是让谷歌平台上的开发者掌握这个新框架&#xff0c;从而让所有应用就有统一的…

Python 常用内置库 time库、random库、turtle库

文章目录 一、time库二、random库三、turtle库1. 绘制正方形2. 使用海龟对象绘制六边形3. 绘制多个起点相同大小不同起点的五角星4. 绘制多个图形和添加文字 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、time库 time是最基础的时间处理库&#…

CAJViewer7.3 下载地址及安装教程

CAJViewer是中国学术期刊&#xff08;CAJ&#xff09;全文数据库的专用阅读软件。CAJViewer是中国知识资源总库&#xff08;CNKI&#xff09;开发的一款软件&#xff0c;旨在方便用户在线阅读和下载CAJ数据库中的学术论文、期刊和会议论文等文献资源。 CAJViewer具有直观的界面…

Kubernetes之Projected Volume

目录 四种Projected Volume Secret 使用方法 应用场景 示例 ConfigMap 使用方法 应用场景 示例 Downward API 使用方法 应用场景 示例 ServiceAccountToken 使用方法 应用场景 示例 在 Kubernetes 中,有几类特殊的 Volume,它们存在的意义不是为了存放容器里的…

深入探索位图技术:原理及应用

文章目录 一、引言二、位图&#xff08;Bitset&#xff09;基础知识1、位图的概念2、位图的表示3、位图操作 三、位图的应用场景1、数据查找与存储2、数据去重与排序 四、位图的实现 一、引言 位图&#xff0c;以其高效、简洁的特性在数据处理、存储和检索等多个领域发挥着举足…

抽象类和接口的简单认识

目录 一、抽象类 1.什么是抽象类 2.抽象类的注意事项 3.抽象类与普通类的对比 二、接口 1.接口的简单使用 2.接口的特性 3.接口的使用案例 4.接口和抽象类的异同 一、抽象类 所谓抽象类&#xff0c;就是更加抽象的类&#xff0c;也就是说&#xff0c;这个类不能具体描…