006 高并发内存池_PageCache设计

news2024/11/29 2:59:15

​🌈个人主页: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);
}

小结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图的遍历试题

一、单项选择题 01.下列关于广度优先算法的说法中&#xff0c;正确的是( ). Ⅰ.当各边的权值相等时&#xff0c;广度优先算法可以解决单源最短路径问题 Ⅱ.当各边的权值不等时&#xff0c;广度优先算法可用来解决单源最短路径问题 Ⅲ.广度优先遍历算法类似于树中的后序遍历算法…

第十五届蓝桥杯模拟考试II_物联网设计

这次写的还是比较顺利的3h完成&#xff0c;一个模块一个模块检查&#xff0c;检查无误后再组装&#xff0c;组装完成后再测试&#xff0c;这样一步一个脚印&#xff0c;将整个模块搭建好是最稳妥的&#xff0c;很少出现一个积木单个没有问题组装之后和体系中的其他积木产生奇妙…

LongAdder 和 AtomicLong

有幸看到一篇关于这个讲解 2个类的讲解&#xff0c;自己也归纳总结一下。 一、解析 看源码底层会发现实现机制不一样&#xff0c;当然这个也是必须的 LongAdder 点进去之后会发现&#xff0c;CAS 它是一个CAS的实现类。至于Cell类JVM提供的内置函数 官方说法是&#xff1a;…

Feign远程调用的基本流程通俗易懂

1. OpenFeign的源码解析 关于OpenFeign的源码解析这位博主写的非常详细&#xff0c;可先阅读该博客【OpenFeign调用服务的核心原理解析】&#xff0c;本文对其内容做了概括整理&#xff0c;较于源码解析 通俗易懂。 2. Feign远程调用的基本流程图解 Feign远程调用&#xff0…

Linux中数据呈现输入输出重定向

1 理解输入输出 目前为止&#xff0c;你已经知道了两种脚本输出的方法&#xff1a; 在显示器屏幕上显示输出 将输出重定向到文件中 1.1 标准文件描述符 Linux系统将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(file descriptor)来标识每个文件对象。文…

【CANN训练营笔记】Atlas 200I DK A2体验手写数字识别模型训练推理

环境介绍 开发板&#xff1a;Huawei Atals 200I DK A2 内存&#xff1a;4G NPU&#xff1a;Ascend 310B4 CANN&#xff1a;7.0 准备环境 下载编译好的torch_npu wget https://obs-9be7.obs.cn-east-2.myhuaweicloud.com/wanzutao/torch_npu-2.1.0rc1-cp39-cp39-linux_aarch…

sql之每日五题day01--多表联查/聚合函数

sql错题记录 含有聚合函数的不能用where升序排列order byleft join多表联查inner join不返回null三表联查 含有聚合函数的不能用where SQL19 分组过滤练习题 题目&#xff1a;现在运营想查看每个学校用户的平均发贴和回帖情况&#xff0c;寻找低活跃度学校进行重点运营&#x…

算法复习:链表

链表定义 struct ListNode { int val;ListNode *next;ListNode(int x) : val(x), next(nullptr) {} }; 链表的遍历&#xff1a;ListNode phead; while(p!null) pp.next; 找到链表的尾结点&#xff1a;phead; while(p.next!null)pp.next; 链表节点的个数&#xff1a; phead…

windows上配置Redis主从加哨兵模式实现缓存高可用

一、哨兵模式 哨兵&#xff08;sentinel&#xff09;是Redis的高可用性(High Availability)的解决方案&#xff1a;由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。当主服务器进入下线状态时&#xff0c;sentinel可以将该主服务器下的某…

基于PHP的新闻管理系统(用户发布版)

有需要请加文章底部Q哦 可远程调试 基于PHP的新闻管理系统(用户发布版) 一 介绍 此新闻管理系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。本新闻管理系统采用用户发布新闻&#xff0c;管理员审核后展示模式。 技术栈&am…

编曲知识15:重复段落编写 尾奏编写 家庭工作室搭建 硬件设备使用常识

15 重复段落编写 尾奏编写 家庭工作室搭建 硬件设备使用常识小鹅通-专注内容付费的技术服务商https://app8epdhy0u9502.pc.xiaoe-tech.com/live_pc/l_6602a586e4b0694cc051476b?course_id=course_2XLKtQnQx9GrQHac7OPmHD9tqbv 重复段落设计 第二段落指代间奏过后的段落 第二…

docker环境中宿主机防火墙添加ssh无法生效的问题分析

背景 在部署了docker容器的环境中&#xff0c;要在防火墙开通22端口&#xff0c;即ssh服务&#xff0c;以便在终端可以正常登陆。使用firewall-cmd在docker区域添加了22端口&#xff0c;但是没有起作用。后再public区域添加22端口才起作用。为什么docker区域不起作用&#xff…

数据结构与算法 顺序栈的基本运算

一、实验内容 编写一个程序sqstack.cpp&#xff0c;实现顺序栈的各种基本运算&#xff0c;并在此基础上写一个程序exp6.cpp,实现以下功能 初始化栈s判断栈是否为空依次进栈元素a,b,c,d,e判断栈是否为空输出出栈序列判断栈是否为空释放栈 二、实验步骤 1、sqstack.cpp 2、ex…

windows-MySQL5.7安装

1.安装包下载 https://downloads.mysql.com/archives/community/&#xff08;社区版下载链接&#xff09; 选择Archives可以下载历史包&#xff0c;此处使用5.7.43 2.解压文件 解压文件到你指定安装的目录&#xff1a;解压完成后在mysql-5.7.43-winx64下新建文件my.ini和d…

python中的deque详解

文章目录 摘要示例1:基本使用示例2:使用maxlen限制队列长度示例3:使用deque实现滑动窗口算法示例 4: 使用 deque 实现旋转数组示例 5: 使用 deque 实现最大/最小栈示例 6: 使用 deque 实现广度优先搜索(BFS)摘要 deque(双端队列)是Python标准库collections模块中的一个…

从0配置React

在本地安装和配置React项目&#xff0c;您可以使用create-react-app这个官方推荐的脚手架工具。以下是安装React的步骤&#xff0c;包括安装Node.js、使用create-react-app创建React应用&#xff0c;以及启动开发服务器。 下载安装node.js运行以下命令&#xff0c;验证Node.js…

《操作系统导论》第16章读书笔记:分段

《操作系统导论》第16章读书笔记&#xff1a;分段 —— 杭州 2024-03-31 夜 文章目录 《操作系统导论》第16章读书笔记&#xff1a;分段0.前言1.分段&#xff1a;泛化的基址/界限2.我们引用哪个段&#xff1f;3.栈怎么办4.支持共享5.细粒度与粗粒度的分段、操作系统支持6.小结7…

从vrrp、bfd、keepalived到openflow多控制器--理论篇

vrrp 在一个网络中&#xff0c;通常会使用vrrp技术来实现网关的高可用。 vrrp&#xff0c;即Virtual Router Redundancy Protocol&#xff0c;虚拟路由冗余协议。 应用场景 典型的如下面这个例子&#xff1a; 当Router故障后&#xff0c;将会导致HostA-C都无法连接外部的I…

嵌入式linux学习之opencv交叉编译

1.下载opencv源码 OpenCV官方源码下载链接为https://opencv.org/releases/&#xff0c;选择3.4.16版本下载。放在ubuntu系统~/opencv文件夹中&#xff0c;解压缩&#xff0c;opencv文件夹中新建build和install文件夹用于存放编译文件和安装文件&#xff1a; 2. 安装编译工具…

springboot配置文件application.properties,application.yml/application.yaml

application.properties Springboot提供的一种属性配置方式&#xff1a;application.properties 初始时配置文件中只有一行语句。 启动时&#xff0c;比如tomcat的端口号默认8080&#xff0c;路径默认。如果我们要改变端口号及路径之类的可以在application.properties中配置。…