接上一篇博客:我们完成了第二层的设计
这个函数帮我们去第三层page_cache去申请一个Span对象回来
n是我们第二层经过 一个控制算法 得出来 第一层向我们申请的时候 我们返回去多少个单位内存
size就是对齐之后的单位内存大小
这两个乘积 就算出来 我们第二层一共需要多少个字节的内存 去给第一层分配
下面我们正式进入第三层的设计:
page_cache申请内存通道设计
1. 当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。(简单理解就是,大的Span可以拆成小的Span)
2. 如果找到_spanList[128]都没有合适的span,则向系统使⽤mmap、brk或者是VirtualAlloc等⽅式 申请128⻚page_span挂在⾃由链表中,再重复1中的过程。
3. 需要注意的是central_cache和page_cache的核⼼结构都是spanlist的哈希桶,但是他们是有本质区别的,central_cache中哈希桶,是按跟thread_cache⼀样的⼤⼩对⻬关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成⼩块内存的⾃由链表。⽽page_cache中的spanlist则 是按下标桶号映射的,也就是说第i号桶中挂的span都是i⻚内存。
先看一下上面三句话,有个大概的理解,我后面会详细展开:
还是从第二层central_cache的那个向下申请的接口函数说起来:
看到这里有一个页流量控制函数
size_t central_cache_page_flow_control(size_t size) // size=个数*单个大小
{
int n = size >> Page_Size_Shift;
if (n < 1)
n = 1;
return n+1;
}
这里我们就比较简单的处理了,这里传进来的size是一共需要多少个字节
所以我们直接用 他 来 除以一个页的大小(128kb就是我们前面设置的) 这里我们用位移操作
因为128kb=128*1024 就是2的13次方 所以这里右移 因为之前也提到过 位移运算要比算术快
也就是呼应一下这里!! 为啥定义了个13
接着说这就好理解了嘛 不够一页给一页
后面的向上取整, 因为c++立马除法是向下取整的嘛 所以加1 免得不够了
然后就正式进入我们的page_cache的代码了:(代码不用全看懂 知道也些什么东西就行 后面会介绍)
#pragma once
#include "common.hpp"
#include <sys/mman.h>
class Page_cache_Span_list
{
public:
Page_cache_Span_list()
{
_head = new struct Span;
_head->_next = _head;
_head->_prev = _head;
}
~Page_cache_Span_list()
{
delete _head;
}
void Push(struct Span *tem)
{
_head->_next->_prev = tem;
tem->_next = _head->_next;
_head->_next = tem;
tem->_prev = _head;
}
struct Span *Pop() // 头删一个
{
auto tem = _head->_next;
_head->_next = tem->_next;
tem->_next->_prev = _head;
return tem;
}
struct Span *Get_head()
{
return _head;
}
private:
struct Span *_head; // 双向带头循环的链表
public:
size_t _flow_control = 1;
};
class Page_cache
{
public:
static Page_cache &Get_instance()
{
return Page_cache_obj;
}
struct Span *Page_cache_Allocate(int size) // size对应的是 几个page的span
{
_mutex.lock();
struct Span *head = _page_cache[size].Get_head();
if (head->_next != head)
{
auto tem = _page_cache[size].Pop();
_mutex.unlock();
return tem;
}
_mutex.unlock();
for (int i = size + 1; i <= Page_Bucket_Size; ++i)
{
_mutex.lock();
struct Span *tem = _page_cache[i].Get_head();
if (tem->_next != tem) // 找到一个大页的span来切小
{
tem = _page_cache[i].Pop();
struct Span *demand = new struct Span;
demand->_initial_address = tem->_initial_address;
demand->_page_id = tem->_page_id;
demand->_page_number = size;
struct Span *surplus = new struct Span;
surplus->_initial_address = tem->_initial_address;
surplus->_page_id = tem->_page_id + size;
surplus->_page_number = tem->_page_number - size;
_page_cache[tem->_page_number - size].Push(surplus);
delete tem;
_mutex.unlock();
return demand;
}
_mutex.unlock();
}
// 走到这里就是需要向OS来申请内存了
void *tem = mmap(nullptr, Page_Size * Page_Bucket_Size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // MAP_PRIVATE | MAP_ANONYMOUS:这是最常见的组合,用于直接向操作系统申请一段当前进程私有的匿名内存:
struct Span *new_span = new struct Span;
new_span->_page_id = 1;
new_span->_initial_address = tem;
new_span->_page_number = Page_Bucket_Size;
_mutex.lock();
_page_cache[Page_Bucket_Size].Push(new_span);
return Page_cache_Allocate(Page_Bucket_Size, size);
}
Span *Page_cache_Allocate(int index, int size)
{
struct Span *tem = _page_cache[index].Get_head();
tem = _page_cache[index].Pop();
struct Span *demand = new struct Span;
demand->_initial_address = tem->_initial_address;
demand->_page_id = tem->_page_id;
demand->_page_number = size;
struct Span *surplus = new struct Span;
surplus->_initial_address = tem->_initial_address;
surplus->_page_id = tem->_page_id + size;
surplus->_page_number = tem->_page_number - size;
_page_cache[tem->_page_number - size].Push(surplus);
_mutex.unlock();
delete tem;
return demand;
}
private:
Page_cache_Span_list _page_cache[Page_Bucket_Size + 1];
static Page_cache Page_cache_obj;
private:
std::mutex _mutex;
};
Page_cache Page_cache::Page_cache_obj;
首先要理解的就是 单例模式 这个和上一层一样 这里就不多赘述了
这里一样封装了一个类 用来管理Span对象的
其实这里你们会发现和上一层 central_cache封装的那个类一模一样 就是成员变量没有锁了,这里其实可以用一样的,无非就是多占一些定义锁的内存 ,我这里重新设置了一个类,把锁拿掉了
为什么把锁拿掉,因为这里需要的一把大锁,直接把这个整体锁住, 因为
这里才申请的过程中,不同的桶之间会有通信也就是交流,所以需要上一把大锁 给他锁住
上一层 每个桶直接是相互独立的 所有只用桶锁就行。
我们讲一下这里的桶结构
这里一共就128个桶 和上面(第二层)做区分了
每一桶里面 依旧挂的还是 Span的双向带头循环的链表
不同的是 这里的在一个桶下面的Span的共同特征是他们具有相同的页数
对比一下第二层(central_cache) 他下面虽然也是Span的双向链表但他们的共同特征是每个Span切分的小内存块大小是一样的
所以第二层需要208个桶 和第一层一样 因为它是按 小内存块的大小进行切分的
而第三层是按span所含的页数来分的
就是这个值
当然了 这里128 就是我们自己设置的 你们可以自己设置
128页就是 8kb*1024就是8M内存 对于我们程序设计来说 这个两已经很大了 800多万字节了
256也是可以的(自行设计)
page_cache的成员变量
一个大锁
这里为啥+1 因为我们设计 希望数组下标为1的就是1个page的
下标为2的 就是下面挂的是 含有2个page的Span 方便阅读嘛 (这个设计从0开始也行 程序员自己定)
下面开始讲解我们申请内存的函数:
一进来 先把锁上起来,保证操作的原子性
拿到我们自身的对象
直接看里面有没有挂对象 有的话就进来 ,直接Pop一个出来 然后解锁返回就行
(因为我们是 双向带头循环的嘛 所以的看 next这一个)
pop头删就很简单了哈
走到这里就是 我们最上面提到的 第一种情况
当前位置没有,我们看一下下面大的 Span有没有 如果有的话 就切一下
这里上锁方式有两种 就是 1、直接for循环外面上个大锁 这样搜索效率最快 ,不会被中断
第二种就是 2、 在for循环里面上锁,这样做的好处就是,向下走的过程中,可能刚好遇到有人还内存,刚好我们就拿到,来切就行 这样做就是 上锁解锁有开销 但是减小了想操作系统申请内存的概率
两种设计方式都行,看你们自己怎么考虑了。
从没有的那一个桶下面开始搜索
如果if条件满足了 就说明找到了一个大的Span 下面就是切分了
切分很简单
就主要围绕这三个东西就行
首先大的Span含有的页肯定多嘛 那就一个Span还有我们需要的页 另一个Span含有剩下的页
然后_initial_address这个地址肯定是一样的
id和所占页数很好理解吧 一个id是不变 那另一个id就是 上一个的id+他所占的页数就行
首先第一步把 我们找到的那个 从page_cache 里面弹出来 因为它被分小的话 ,那所占这个页数的Span肯定就是消失的,所以要弹出来
然后就是new来个Span出来 一个作为返回值传给central_cache 另一个 剩下的我们就push会page_cache里面
这个就相当于我们返回的
剩的那一个 id就是大的那个id+我们返回了多少个页
因为上一讲我们知道的 一个Span是包含的一段连续的页 所以这里直接线性加减就行
然后就是push回一个 解锁 然后返回我们需要的那个Span
这里为啥我们只关注了
这三个量
因为
其他的量都在 使用他们的地方设置 ,所以这里还有一个细节的地方
这个东西你如果不在定义的时候 给一个初始值 ,那你就必须在new Span对象的时候设置,不然就是随机值,因为我们后面申请一个内存走 ,就直接 _useCount++
所以这里一定要设置!!!
程序继续向下走的话 就是page_cache里面也没有了 ,那我们就需要想操作系统申请了。
这里为啥不用malloc来申请内存
总的来说 就是malloc其实底层用的还是mmap或者brk
既然提到这里了,就简单说说吧, 你以为你malloc一个大小的内存块,他返回回来的就是你申请的大小么 他的底层和我们这里设计的也是一样,有一个对齐方式, 他也是一次性向系统申请一大块,然后你申请他就给你一点 。和我们这个逻辑是一样的,但是他设计的非常优秀,除了多线程高并发的时候,其他的时候都是天花板级别的
言归正传,
这里的 mmap和brk 是Linux系统里面的话 Windows的用VirtualAlloc
函数具体怎么用 查查手册 gpt一下 很容易
具体里面参数都代表啥意思 我就不多说了 gpt一下 然后拿到了这段地址的首地址
然后就是new 一个Span对象,然后把首地址填进去,id为1 也是就是 你申请的页数
这里有同学会问了 这里你申请多少个页就是多少个页么
当然不是了,并且这里操作系统根本就没有页这个概念,他就知道你申请了这么多字节,他给你返回来的是一个大于等于这么多字节的内存块的首地址,
所以后面我们按页来分 ,不过就是为了更好的管理内存
先描述在组织!!!
相当于数学建模一样,你得为他建立一个描述他的模型
最后就是push进我们的page_cache里面 这里要加加锁处理,
然后我重载了这个函数
相当于从而又回到了1
我这里是一直加锁状态的,直接让他在最大的位置(刚申请的位置)来获取然后直接切分
在传回去
有同学疑惑了,为啥每次找操作系统申请内存 要申请最大的页,因为我们大页可以拆小使用,并且你申请内存是有开销的,我们本来设计的就是高并发内存池,向操作系统频繁的去要,会影响效率的,所以每次我们直接申请一大块内存,就和malloc的实现一样,拿一大块内存我自己来管理。
走到这里就衔接我们的第二层(central_cache)了,把span传回去 然后切割
各位看到这里,对于我们的申请内存这一通道基本没什么问题了,下面就是各位把代码按自己的理解写出来,并调试一下,看看申请的通道是怎么串起来的
从下一篇博客开始就是我们的释放通道了,加油各位 咱们一起共勉
释放通道三层设计:
见博客