项目:高并发内存池(tcmalloc轻量版)——3

接上一篇博客:我们完成了第二层的设计

这个函数帮我们去第三层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传回去 然后切割

各位看到这里,对于我们的申请内存这一通道基本没什么问题了,下面就是各位把代码按自己的理解写出来,并调试一下,看看申请的通道是怎么串起来的

从下一篇博客开始就是我们的释放通道了,加油各位 咱们一起共勉

释放通道三层设计:

见博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值