七、高并发内存池--Page Cache

news2025/1/16 1:03:23

七、高并发内存池–Page Cache

7.1 PageCache的工作原理

PageCache是以span的大小(以页为单位)和下标一一对应为映射关系的哈希桶,下标是几就说明这个哈希桶下挂的span的大小就是几页的,是绝对映射的关系。因为PageCache也是全局只有唯一一个的,所以为了防止出现线程安全的问题,在访问PageCache之前要先加上一把大锁锁住整个PageCache,为什么这里是加一把大锁锁住整个PageCache而不是加桶锁锁住要访问的那个桶呢?

因为当Central Cache向PageCache获取一个K页的span的时候,在PageCache中下标为K的哈希桶中不一定有span,此时PageCache并不是直接向系统堆中申请一块K页的span内存,因为如果这样处理的话会出现频繁地向系统申请内存的情况,并且每次向堆申请的内存都只是K页大小的,如果每一次的K都是1或者2,这样会使堆申请出来的内存都是很碎片化的,不便于堆做内存管理,所以PageCache的处理策略是如果当前下标的哈希桶中没有span对象,那么就从当前位置开始往后遍历,如果后面的哈希桶中有span对象就把这个span切成一个K页的kspan和一个(n-k)页的span,把kspan切成一个一个的小对象连接到kspan中的自由链表并把这个切好的kspan返回给CentralCache对应位置的哈希桶,剩下的(n-k)页大小的span就挂到PageCache中n-k位置的哈希桶。

如果把PageCache中剩下的所有的哈希桶都遍历完都没有找到一个span,那么就向系统申请一个128(这个128是自定义的)页大小的大span,重复上面的切分操作,最后返回一个切好的K页的span给CentralCache对应位置的哈希桶中,再让CentralCache分若干个小对象给ThreadCache,ThreadCache再返回一个给上层。

综上,也就不难发现PageCache不能用桶锁而是要用一把大锁,因为在CentralCache向PageCache中获取一个span时不仅仅是会访问PageCache中的某一个位置的哈希桶,而是有可能往后遍历其它的哈希桶并访问,所以加桶锁只是锁住了一个桶,并不能锁住后续遍历的其它位置的哈希桶,所以这里要加一把大锁。其实从技术上来说用桶锁也是可以的,只不过在遍历PageCache后面的每一个哈希桶前都要对先对这个桶加上桶锁,访问完之后还要解掉桶锁,如此一来,必然会出现大量的上锁解锁的操作,反而会降低了内存池的效率,所以加一把大锁就行了。

在这里插入图片描述

7.2 PageCache.h

//页缓存,CentralCache需要向PageCache申请K页大小的span,PageCache向系统申请128页大小的span
//因为PageCache也是全局只有唯一一个的,所以也把PageCache设计成单例模式
class PageCache
{
public:
	static PageCache* GetInstance()
	{
		return &_sInst;
	}

	//提供给CentralCache向PageCache获取一个k页的span时使用
	Span* NewSpan(size_t k);

	//根据obj的地址转换成页号,进而通过哈希表找到页号对应的Span
	Span* MapObjectToSpan(void* obj);

	//CentralCache把span还回来给PageCache
	void ReleaseSpanToPageCache(Span* span);

	//PageCache是整个进程唯一的,所以所有的线程都有可能
	//访问PageCache,存在线程安全问题,所以需要加锁,这里是加一把大锁而不是桶锁
	std::mutex _pageMtx;

private:
	//单例模式需要把构造函数私有化,防止别人创建对象
	PageCache()
	{}

	PageCache(const PageCache&) = delete;

private:
	SpanList _spanLists[NPAGES];//有多少也大小的span数组就有多大,下标和span的大小一一对应

	//声明静态对象
	static PageCache _sInst;

	//如果PageCache中所有的哈希桶都没有span,需要从系统堆中申请内存时
	//要脱离malloc,所以直接用我们写好的定长内存池申请内存即可,定长内存
	//池中封装了系统调用接口,直接向堆申请内存和释放内存
	ObjectPool<Span> _spanPool;

	//保存页号和span的映射关系,为了后续能找到每一个还回来的小对象属于哪一个span
	std::unordered_map<PAGE_ID, Span*> _idSpanMap;

};

7.3 PageCache.cpp


//静态对象需要在类内声明,类外面定义
PageCache PageCache::_sInst;

//k代表的是这个span的大小k页
Span* PageCache::NewSpan(size_t k)
{
	assert(k > 0);

	//如果申请的span的大小大于128页,则需要直接向堆申请
	//因为PageCache只有128个有效的哈希桶
	if (k > NPAGES - 1)
	{
		//向堆申请k页内存
		void* ptr = SystemAlloc(k);
		Span* kSpan = _spanPool.New();//_spanPoll是定长内存池对象,直接向堆申请内存
		//地址转化成页号
		kSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
		kSpan->_n = k;
		//把页号和Kspan的映射关系放进Map中
		_idSpanMap[kSpan->_pageId] = kSpan;
		return kSpan;
	}
	else
	{
		//如果PageCache第k个位置的哈希桶上有k页大小的span,则直接返回一个span
		if (!_spanLists[k].Empty())
		{
			Span* kSpan = _spanLists[k].PopFront();

			//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
			//CentralCache回收小块内存时,查找对应的span
			//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
			//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
			// 
			//这里曾经没有存映射关系,导致归还小内存块的时候找不到所属的span(细节)
			for (PAGE_ID i = 0; i < kSpan->_n; i++)
			{
				_idSpanMap[kSpan->_pageId + i] = kSpan;
			}

			return kSpan;
		}

		//走到这里说明PageCache第k个位置的哈希桶没有k页大小的span,则需要遍历
		//后面的大于k页的哈希桶,找到了一个n页大小的span就把这个span切分成一个
		// k页大小的span和一个n-k页大小的span,k页的返回,n-k页的挂到对应的哈希桶中

		//遍历后面的哈希桶
		for (size_t i = k + 1; i < NPAGES; i++)
		{
			//找到了一个不为空的i页的哈希桶,就对它进行切分
			if (!_spanLists[i].Empty())
			{
				//k页的span
				Span* kSpan = _spanPool.New();
				//n页的span
				Span* nSpan = _spanLists[i].PopFront();//把这个i页大小的span拿出来进行切分

				//开始把一个n页的span切分成一个k页的span和一个n-k页的span
				// 
				//从nSpan的头上切k页给kSpan,所以kSpan的页号就是nSpan的页号
				kSpan->_pageId = nSpan->_pageId;
				kSpan->_n = k;//kSpan的页数是k

				//被切分以后nSpan的页号需要+=k页,因为头nSpan的头k页已经切分给了kSpan
				nSpan->_pageId += k;
				nSpan->_n -= k;//nSpan的页数要-=k页,因为nSpan被切走了k页

				//把kSpan的页号和对应的Span*的映射关系存放到哈希桶中去,方便
				// CentralCache回收小块内存时,查找对应的span
				//kSpan代表的是一个k页大小的Span的大块内存,kSpan->_pageId
				//代表这个大块内存的起始地址,有k页,所以这k页映射到的都是这个Span
				for (PAGE_ID i = 0; i < kSpan->_n; i++)
				{
					_idSpanMap[kSpan->_pageId + i] = kSpan;
				}

				//nSpan被切分后的首页和尾页的页号和nspan的映射关系也需要保存起来
				//以便后续合并,因为合并的方式是前后页合并,往前找肯定找到的是一个span的
				//最后一页,往后找一定找的是一个span的第一页,所以挂在PageCache对应哈希桶
				//的span的第一页和最后一页与span的关系也需要保存起来
				_idSpanMap[nSpan->_pageId] = nSpan;
				_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;


				//把剩余的n-k页的span头插到对应下标的哈希桶中,nSpan->_n已经是改过的了,不用再-k
				_spanLists[nSpan->_n].PushFront(nSpan);
				return kSpan;
			}
		}

		//走到这里说明前面的NPAGES个哈希桶中都没有Span,(例如第一次申请内存时)
		//则需要向堆申请一个128页大小的span大块内存,挂到对应的哈希桶中
		void* ptr = SystemAlloc(NPAGES - 1);

		Span* bigSpan = _spanPool.New();
		bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//内存的地址需要转换成页号映射到对应的哈希桶中
		bigSpan->_n = NPAGES - 1;//大块内存的页数,描述这个bigspan的大小

		//把NPAGES-1页大小的span头插到对应NPAGES-1号桶中去
		_spanLists[bigSpan->_n].PushFront(bigSpan);

		//本质是运用了复用的设计,避免代码中出现重复的逻辑
		return NewSpan(k);
	}

}

Span* PageCache::MapObjectToSpan(void* obj)
{
	//计算出obj对应的页号
	PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;

	//访问_idSpanMap的时候需要加锁,避免线程安全的问题
	//这里使用C++11的RAII锁,出了这个函数这把锁会自动解掉
	std::unique_lock<std::mutex> lock(_pageMtx);

	//通过页号查找该内存块对应的是哪一个span
	auto ret = _idSpanMap.find(id);
	if (ret != _idSpanMap.end())
	{
		return ret->second;
	}
	else
	{
		assert(false);
		return nullptr;
	}
}

//CentralCache把span还回来给PageCache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
	//如果span的页数大于128页,则说明这个span是从堆上直接申请的,
	//直接释放给堆即可,不能挂到PageCache的哈希桶中,因为PageCache一个只有128个桶
	if (span->_n > NPAGES - 1)
	{
		void* ptr = (void*)(span->_pageId << PAGE_SHIFT);
		SystemFree(ptr);
		_spanPool.Delete(span);
		return;
	}

	while (1)
	{
		//找前一页的span,看是否能够和当前页合并,如果能,则循环向前合并,直到不能合并为止
		PAGE_ID prevId = span->_pageId - 1;
		auto ret = _idSpanMap.find(prevId);

		//_idSpanMap中没找到前一页和对应span,说明前一页的内存没有被申请,结束合并
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* prevSpan = ret->second;
		//如果前一页对应的span在CentralCache中正在被使用,结束合并
		if (prevSpan->_isUse == true)
		{
			break;
		}

		//如果和前一页合并之后会超过哈希桶的最大的映射返回,结束合并
		if (prevSpan->_n + span->_n > NPAGES - 1)
		{
			break;
		}

		//合并span和prevSpan
		span->_pageId = prevSpan->_pageId;
		span->_n = prevSpan->_n + span->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[prevSpan->_n].Erase(prevSpan);
		//因为prevSpan已经被合并到了span中,所以prevSpan对应的内存可以delete掉了
		_spanPool.Delete(prevSpan);

	}

	while (1)
	{
		//找span的下一个span的起始页号
		PAGE_ID nextId = span->_pageId + span->_n;
		auto ret = _idSpanMap.find(nextId);
		if (ret == _idSpanMap.end())
		{
			break;
		}

		Span* nextSpan = ret->second;
		if (nextSpan->_isUse == true)
		{
			break;
		}

		if (nextSpan->_n + span->_n > NPAGES - 1)//曾经写成NPAGES+1了
		{
			break;
		}

		//span的起始页号不变,页数相加
		span->_n = span->_n + nextSpan->_n;
		//合并之后需要把prevSpan在对应的哈希桶中删除掉
		_spanLists[nextSpan->_n].Erase(nextSpan);
		_spanPool.Delete(nextSpan);
	}

	//合并得到的新的span需要挂到对应页数的哈希桶中
	_spanLists[span->_n].PushFront(span);
	//在PageCache中的span要设置为false,好让后面相邻的span来合并
	span->_isUse = false;
	//为了方便后续的合并,需要把span的起始页号和尾页号和span建立映射关系
	_idSpanMap[span->_pageId] = span;
	_idSpanMap[span->_pageId + span->_n - 1] = span;

}

以上就是高并发内存池三层模型的核心部分,后面的文章是对该高并发内存池的性能测试以及针对性能的瓶颈区做出相应的优化策略。

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

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

相关文章

VMware 设置仅主机模式无法访问外网的问题说明

参考链接 VMware仅主机模式访问外网 如果根据以上参看仍旧无法访问物理机网段其他设备以及无法访问外网&#xff0c;可以尝试在虚拟机上根据 vmnet1 网卡设置的 ip 地址添加默认路由&#xff0c;如下图所示&#xff1a; 首先查看对应网卡设置的 ip 地址&#xff08;博主为啥…

Java从入门到精通-流程控制(一)

流程控制 1.复合语句 复合语句&#xff0c;也称为代码块&#xff0c;是一组Java语句&#xff0c;用大括号 {} 括起来&#xff0c;它们可以被视为单个语句。复合语句通常用于以下情况&#xff1a; - 在控制结构&#xff08;如条件语句和循环&#xff09;中包含多个语句。 - …

MYSQL(索引、事务)

文章目录 一、索引二、事务 一、索引 数据库中的表、数据、索引之间的关系&#xff0c;类似于书架上的图书、书籍内容和书籍目录的关系 1. 概述 概念&#xff1a;相当于是一本书的目录&#xff0c;是以‘列’为维度进行建立的使用场景&#xff1a;如果我们要查询一个表中的某个…

管理类联考——逻辑——汇总篇——知识点突破——形式逻辑——性质模态——负命题

角度 角度——汇总 角度——汇总 矛盾关系 性质 (1) 所有的 S 是 P 所有的S是P 所有的S是

Linux中创建文件夹,删除文件夹

Linux中创建目录&#xff1a;mkdir 文件夹&#xff0c; 比如&#xff1a;mkdir test 删除文件夹&#xff1a;rm -rf 文件夹&#xff0c; 比如&#xff1a;rm -rf soft vi强制不保存退出命令&#xff1a;q&#xff01;

2023开学礼《乡村振兴战略下传统村落文化旅游设计》北农馆藏许少辉八一新书

2023开学礼《乡村振兴战略下传统村落文化旅游设计》北京农学院图书馆许少辉八一新书

HTTP协议详解:互联网通信背后的规则与秘密

个人主页&#xff1a;insist--个人主页​​​​​​ 本文专栏&#xff1a;网络基础——带你走进网络世界 本专栏会持续更新网络基础知识&#xff0c;希望大家多多支持&#xff0c;让我们一起探索这个神奇而广阔的网络世界。 目录 一、HTTP协议的基本概念 二、HTTP协议的主要特…

npm报错sass

1.删除node模块 2.删除node-sass&#xff1a; npm uninstall node-sass 3.重新下载对应版本node-sass&#xff1a; npm i node-sass7.0.3&#xff08;指定版本 控制台报错什么版本就写什么版本&#xff09; 4.再运行项目 或者

Linux学习之lvm删除

umount /mnt/logicvolumntest卸载挂载。 lvremove /dev/vgname/my_lv可以删除逻辑卷&#xff0c;其中vgname是指定逻辑卷所在的卷组名称&#xff0c;my_lv是逻辑卷的名称。 注意&#xff1a;使用lvremove命令会永久删除逻辑卷和其中的数据&#xff0c;因此请在使用之前进行适当…

【人工智能】—_深度优先搜索、代价一致搜索、深度有限搜索、迭代深度优先搜索、图搜索

【人工智能】无信息搜索—BFS 、代价一致、DFS、深度受限、迭代深入深度优先、图搜索 什么是搜索 搜索问题是指既不能通过数学建模解决&#xff0c;又没有其他算法可以套用或者非遍历所有情况才能得出正确结果。这时就需要采用搜索算法来解决问题。搜索就是一种通过穷举所有解…

【分布式搜索引擎es】

文章目录 数据搜索DSL实现查询文档搜索结果处理 RestClient实现 elasticsearch最擅长的是 搜索和 数据分析。 数据搜索 DSL实现 查询文档 常见的查询类型包括&#xff1a; 查询所有&#xff1a;查询出所有数据&#xff0c;一般测试用。例如&#xff1a;match_all全文检索…

Power BI 连接 MySQL 数据库

Power Query 或 Power BI 只提供了对 SQL Server 的直接连接&#xff0c;而不支持其它数据库的直连。所以第一次连接 MySQL 数据库时&#xff0c;就出现下面的错误信。 这就需要我们自己去安装一个连接器组件。https://downloads.mysql.com/archives/c-net/ 错误解决方案 我一…

【已解决+吐槽】pip install cn2an报错 Cannot uninstall ‘ruamel_yaml‘

我需要用cn2an模块将中文的数字转化为阿拉伯数字&#xff0c;但在安装cn2an的过程中出现了以下报错&#xff1a; 于是乎&#xff0c;我跟着CSDN上诸如此类的教程开始跟nodejs死磕&#xff0c;折腾了大半天&#xff0c;以下是各种尝试。这不是重点&#xff0c;我主要是吐槽&…

Spring MVC 五 - Spring MVC的配置和DispatcherServlet初始化过程

今天的内容是SpringMVC的初始化过程&#xff0c;其实也就是DispatcherServilet的初始化过程。 Special Bean Types DispatcherServlet委托如下一些特殊的bean来处理请求、并渲染正确的返回。这些特殊的bean是Spring MVC框架管理的bean、按照Spring框架的约定处理相关请求&…

传送带下料口堵塞识别检测算法 yolov5

传送带下料口堵塞识别检测算法通过python基于yolov5网络深度学习框架模型&#xff0c;下料口堵塞识别检测算法能够准确判断下料口是否出现堵塞现象&#xff0c;一旦发现下料口堵塞&#xff0c;算法会立即抓拍发出告警信号。Python是一种由Guido van Rossum开发的通用编程语言&a…

《信息系统项目管理师教程(第4版)》第17章 采购管理、合同管理 知识点整理,xmind思维导图

已上传采购管理xmind思维导图&#xff0c;需要的同学可以直接下载哦。 一、规划采购管理 二、实施采购 三、控制采购 四、合同管理 4.1 合同类型 4.2 合同管理过程 签订履行变更档案&#xff0c;合同档案管理是整个合同管理的基础&#xff0c;要求采用电脑打印文本&#xff…

在k8s中用label控制Pod部署到指定的node上

案例-标注k8s-node1是配置了SSD的节点 kubectl label node k8s-node1 disktypessd 查看标记 测试 将pod部署到disktypessd的节点上&#xff08;这里设置了k8s-node1为ssd&#xff09; 部署后查看结果-副本全都运行在了k8s-node1上—符合预期 删除标记 kubectl label node k8…

yolov8机器视觉-工业质检

使用训练好的模型进行预测 yolo predict taskdetect model训练好的模型路径 source测试图片文件夹路径 showTrue效果展示 切换模型进行训练&#xff08;yolov8s&#xff09; 修改main.py训练参数文件 使用云gpu进行训练&#xff0c;很方便&#xff1a;点击链接转至在线云gpu…

2020年下半年系统架构设计师上午真题及答案解析

1.按照我国著作权法的权利保护期&#xff0c;&#xff08; &#xff09;受到永久保护。 A.发表权 B.修改权 C.复制权 D.发行权 2.假设某计算机的字长为32位&#xff0c;该计算机文件管理系统磁盘空间管理采用位示图记录磁盘的使用情况&#xff0c;若磁盘的容量为3…

python节假日库holidays——查询国家节假日

节假日—计算某天是否为节假日 参考学习&#xff1a; ​ Python holidays模块 ​ Python实现节假日查询 ​ Python怎么获取节假日信息 pip install holidaysimport holidayscn_holidays holidays.CountryHoliday(CN) print(cn_holidays)from datetime import dateif date(…