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

接上一篇文章:

central_cache设计:

上一篇文章 我们完成了  thread_cache的设计:  总体来说就是 thread_cache 里面没有内存申请时,会向下一层  申请一些内存: 

central_cache也是⼀个哈希桶结构,他的哈希桶的映射关系跟thread_cache是⼀样的。不同的是他的 每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下⾯的span中的⼤内存块被按映射关系切成 了⼀个个⼩内存块对象挂在span的⾃由链表中。

申请内存:

1. 当thread_cache中没有内存时,就会批量向central_cache申请⼀些内存对象,这⾥的批量获取对象 的数量使⽤了类似⽹络tcp协议拥塞控制的慢开始算法;central_cache也有⼀个哈希映射的 spanlist,spanlist中挂着span,从span中取出对象给thread_cache,这个过程是需要加锁的,不 过这⾥使⽤的是⼀个桶锁,尽可能提⾼效率。(就是每一个桶,有一个锁)

2. central_cache映射的spanlist中所有span的都没有内存以后,则需要向page_cache申请⼀个新的 span对象,拿到span以后将span管理的内存按⼤⼩切好作为⾃由链表链接到⼀起。然后从span中取对象给thread_cache。

3. central_cache的中挂的span中use_count记录分配了多少个对象出去,分配⼀个对象给thread_ cache,就++use_count。 (这一点先不用了解   给释放的时候 回收内存 使用的)

现在开始正式介绍一下:

 这里的大小划分还是 和  上一层thread_cache里面一样   因为thread_cache 直接申请的就是固定大小的,所以这里延续这一个风格。

不同的就是  这个里面的桶不再是直接放自由链表 而是放的是一个   结构体  struct span 

struct Span
{

  void *_initial_address; //一方面用来计算起始地址  一方面用来标识作用
  size_t _page_id;        // 起始片id      其实id从1开始 就是第一片 第二片 第三片 ……
  size_t _page_number;

  struct Span *_next;
  struct Span *_prev;

  void *_free_list; // 切成小块内存的自由链表 头结点
  size_t _useCount=0;
};

这里先解释一下 为啥 多封装一个结构体  而不是直接像第一层  直接放自由链表    主要是为下一层服务的  因为多线程你向操作系统申请内存 是要加锁的 所以每次申请内存 我们多向操作系统申请一大块 然后我们管理   这也就是 多线程环境下 比malloc优秀的原因。

一次性申请一大块   又要分208个不同大小的固定内存块  直接来管理是不方便  ,这里就引入struct Span 来进行管理     作为中间层 链接上下层  所以为什么设计成三层都是有道理的。

这个是描述 从操作系统申请一大块内存的首地址 ,一方面后面用他来计算 切分之后 各个内存的首地址的 , 一方面 是用来 区分哪些Span是属于同一个大内存块的(因为属于同一个大内存 这个地址肯定一样 )  方便后面释放还给系统。

这个就是 用来 切分大内存块的   

这里要引入一个名词  就是   页      也是为了更好的管理大块内存的

不用过去深究   你就记住:我们从操作系统申请一大块内存(就理解为:malloc一个很大的内存 ,  实际用的不是malloc  是 mmap  你就理解为malloc就行 )

把这一大块内存 切分成若干小份      (这里的一小份 我们就叫做页)

最后把  连续的若干页  组成一个Span

这就是三者的关系,这么讲就很明了

红色是申请的一大块内存

绿色是 大块切成若干小块的一块 (因为申请的大块是我们自己控制的 所以可以保证是页的整数倍)

黄色是 若干个连续的页组成的 结构体 Span

所以这个page_id就是 上面的绿色框页 从左到右 12345……这样编号下去,_page_number这个表示的是一个span里面有几个页

为什么这么处理呢?

其实就是 有了这三个参数  我可以知道任意一个Span对象里分的内存的首地址     _initial_address+(_page_id*-1)*(一页的大小)

这个就很好理解了   因为我这里挂的是 双向带头循环的   所有得有前后指针

这个就表示的是  span所管理的那一部分的内存的起始地址 

useCount表示 这部分内存被使用了几个(后面用来回收内存的时候使用 后面详细展说)

因为我们最终 还是要把Span所管理的内存  切成固定大小的内存块 然后用自由链表把这些连起来

有了struct span之后  我们类似thread_cache一样 在封装一个类来管理他

class Central_cache_Span_list
{
public:
    Central_cache_Span_list()
    {
        _head = new struct Span;
        _head->_next = _head;
        _head->_prev = _head;
    }

    void Push(struct Span *tem)
    {
        _head->_next->_prev = tem;
        tem->_next = _head->_next;
        _head->_next = tem;
        tem->_prev = _head;
    }

    struct Span *Get_head()
    {
        return _head;
    }

    ~Central_cache_Span_list()
    {
        delete _head;
    }

private:
    struct Span *_head; // 双向带头循环的链表

public:
    std::mutex _mutex; // 桶锁
    size_t _flow_control = 1;
};

因为是双向带头循环结构的链表 所以构造函数要new一个对象  析构函数要释放new的对象

然后这里从下层申请到一个Span的话  得头插到我们对应位置的链表里面  封装一个接口

给每一个桶加一个锁   就是我们前文提到的桶锁

_flow_control 就是一个控制流量大小的  (后面在介绍)

然后就是central_cache的代码了:  (不用全看懂 大致看一下有哪些东西 我下面会逐一介绍的)

class Central_cache // 饿汉模式 比较适合场景   因为最开始 这个内存池里面是啥都没有的 所以这里的每一层都会走一遍
{
public:
    static Central_cache &Get_instance()
    {
        return Central_cache_obj;
    }

    void *Central_cache_Allocate(size_t index, size_t size)
    {
        // 每次不可能thread_cache 要一个给一个  这里他要 我就一次性多给几个   涉及一个  慢开始算法
        _central_cache[index]._mutex.lock();
        int n = std::min(central_cache_flow_control(size), _central_cache[index]._flow_control);
        if (n == _central_cache[index]._flow_control)
            _central_cache[index]._flow_control *= 2;

        struct Span *head = _central_cache[index].Get_head();
        while (head->_next != _central_cache[index].Get_head() )  //注意这里判断是不是回到了头部
        {
            head = head->_next;
            void *left;
            void *right;
            if (head->_free_list)
            {
                left = head->_free_list;
                right = left;
                int i=1;
                while ((*(void **)right) != nullptr && --n)
                {
                    right = *(void **)right;
                    i++;
                }
                head->_free_list = *(void **)right;
                *(void **)right = nullptr;
               head->_useCount+=i;
                _central_cache[index]._mutex.unlock();
                return left;
            }
        }
        _central_cache[index]._mutex.unlock();


        // 走到这里就是需要去 page_cache 里面去申请内存
        Span *tem = Page_cache::Get_instance().Page_cache_Allocate(central_cache_page_flow_control(n * size));
        // 对Span进行切小
        void *start = (void *)((tem->_page_id - 1) * Page_Size + (size_t)(tem->_initial_address));

        tem->_free_list = start;

        void *end = (void *)((tem->_page_number * Page_Size / size) * size + (size_t)start); // 这里会有内存碎片 为了下面的while不越界访问 这里要把内存碎片消除 求边界点
        while ((void *)((size_t)start + size) < end)                                          //这里移出去的碎片 不用管   反正它依旧在这里 后面合并成大的也 直接就还给
        {
            (*(void **)start) = (void *)((size_t)start + size);
            start = (void *)((size_t)start + size);
        }
        (*(void **)start) = nullptr;


        _central_cache[index]._mutex.lock();
        _central_cache[index].Push(tem);
        return Central_cache_Allocate(index, size, n);
    }

    void *Central_cache_Allocate(size_t index, size_t size, int n)
    {
        struct Span *head = _central_cache[index].Get_head();
        while (head->_next != head)
        {
            head = head->_next;
            void *left;
            void *right;
            if (head->_free_list)
            {
                left = head->_free_list;
                right = left;
                int i=1;
                while ((*(void **)right) != nullptr && --n)
                {
                    right = *(void **)right;
                    i++;
                }
                head->_free_list = *(void **)right;
                *(void **)right = nullptr;
                head->_useCount+=i;
        _central_cache[index]._mutex.unlock();
                return left;
            }
        }
        _central_cache[index]._mutex.unlock();
        return nullptr;
    }

    // void Central_cache_Deallocate()
    // {

    // }

private:
    Central_cache()
    {
    }

    Central_cache(const Central_cache &) = delete;

    Central_cache &operator=(const Central_cache &) = delete;

private:
    Central_cache_Span_list _central_cache[Hashi_Bucket_Size];

    static Central_cache Central_cache_obj;
};

Central_cache Central_cache::Central_cache_obj;

 上一篇文章我们提到了   每一个线程都有一个thread_cache 所以这里不用加锁  

但是他们共用一个central_cache  也 共用一个   page_cache 

所以这里设计 我们采用单例模式 而且要加一个锁    为什么这里加桶锁呢? 因为每个桶里面的Span管理着不同大小的固定内存 所以之间互不影响,只有在 两个线程都要申请相同大小的内存块时,才会有竞争问题,所有这里每个桶一个锁,而不是整体一个大锁,是为了提高效率。

单例模式 我们采用  饿汉模式  为啥不用懒汉模式   就是因为反正每一层都会走一遍的,因为刚开始申请内存的时候,三层里面都是空的  ,所以采用恶汉模式,程序一运行,就把三层创建好,而不是等要申请内存的时候才来创建。

 单例模式:

这么设计是为了  只有一个对象的生成 防止拷贝 

用这个类 创建一个 静态成员变量   然后再类外定义它。因为静态变量,在重新一开始运行就实例化了。

然后再用一个静态成员函数来获取这个类的实例

(恶汉模式不懂的   gpt一下 代码稍微看一下 就懂  我这里就不详细展开说了)

对于我们现阶段 只关注申请内存这一通道  所以就这一个函数

下面我们就详细展开说说 这个函数:

先看传进来的两个参数是什么:  thread_cache 里面  

index是   映射出来的下标    size是对齐之后的大小       其实这两个就是一个意思  一一对应的嘛  只不过程序他不知道 我们得传进来

我们刚刚又说了  central_cache  和 thread_cache 他们的桶设置的大小 是一样的  所以index是通用的,所以这里直接拿进来用,其实只传一个size也可以,反正我们有一个函数可以来算index,但是为了效率,我们把index传进来 ,因为高并发场景,会有大量的申请释放,每次都重复计算,也是一种开销。

下面开始介绍这个函数的设计:

这里最开始  就涉及到一个 慢开始算法,因为我们不可能他要一个我们就给一个,因为这样效率太低,这一层我们是有锁的,如果每一次thread_cache 里面没了 每次拿一个 ,不如他要 我就多给几个让他下次可以无锁的直接在thread_cache里面自己拿,效率不就上来了 。

一次多给几个,我们以二倍的增长来给他  比如说 第一次申请 我们就给1个  再来申请 我们就给 2个  再来申请就给 4个  这样下去,但是不能无限制增长 2的10次方都1024了  所以得设置一个上限

就是min函数里面的  central_cache_flow_control()

size_t central_cache_flow_control(size_t size) // size为需要去申请的大小    为每次去central_cache申请内存设置一个上限
{
  if (size == 0)
    return 0;

  int n = MAX_Thread_memory / size;
  if (n < 2)
    n = 2;
  if (n >= 512)
    n = 512;
  return n;
}

MAX_Thread_memory 就是我们上篇文章里面提到的256kb

这个算法就是 size越大我保证最小 你每次申请有两个 ,越小 我不让你一次性申请超过512个

相当于就是 你申请的内存块越大 多给你几个   ,  申请的越小少给你几个 。 (方法不唯一,可以暴力一点,直接设置申请的这个个数,最小一个,最大设置一个上限就行)

这里就是获得 我管理的  那个 span链表的头节点!   因为index已经让我们知道该从哪个span里面获取链表了。

双向带头循环的链表,我们就开始遍历一下

如果里面有我们就 拿到他

因为这个是存放自由链表的地方 就看他是不是为空就知道 这个span里面还有没有  内存对象拿   

如果有 就进来拿:

这个条件就是  你拿n个 或者 就是你把里面全拿走了(不够的情况)     这两种情况嘛

任意哪种情况都无所谓嘛  反正我只要进来了  就至少拿了一个了(所以i初始化为1)  直接给thread_cache 传回去,反正也完成这次他的需求了,在申请 再让thread_cache 找我来拿就行。

用来统计 这个Span对象里面被拿走了多少个

访问内存块的前几个字节 拿到下一个内存块的地址

前文提到过 就不细说了 

在解释一下为啥--n  不是要n个么 

不够的情况同理!!! 

循环退出来  right前几个字节 要么为空 走到结尾了  要么指向下一个内存块的地址  所以把他传回给span   然后再把它置为空 ,因为要把链表的最后一个内存块 指向空 来标识结尾

然后解锁之后返回。

再往后走 就说明   现在的central_cache也有没有我们需要的内存 ,所以先解锁,再往下一层page_cache 里面去申请。

这个函数不用管  反正现在你知道 他会给我传回一个Span对象(里面有若干个页的大块内存)

拿到这个Span了 下来我们就要对Span进行切片了  因为他不是我们要的 那种固定大小的小内存块(不是这种形式 ,而是 我们上面提到的 一大块内存)

首先我们先求 这个span的起始地址,这个上面提过   span里面的内容就支持我们计算了

以为是地址所以计算前先强转为  整数类型  计算玩 在转回指针类型

const static size_t Page_Size_Shift = 13;
const static size_t Page_Size = 1 << Page_Size_Shift; // 一页的大小为8kb

这里要给出每一个页的大小了  我们这里设置的是8kb  也就是2的13次方  (这里和内存管理那里的页框没啥关系哈)  就是随便设置的    你也可以自己设置这个页的大小  就是一个中间变量的作用

为啥这里定义了一个13  因为后面还要用它  并且计算机里面 位移运算要比 乘除  稍微快  所以这里设置了个13 

言归正传:

这一步是极容易埋坑的哈, 我们申请的span是含有若干个页的大小哈 无非就是8kb的整数倍

但是我们的size 从  8  16  24 一直到 256kb的哈   是无法保证 每一个span都能整除的哈

所以这样处理一下就是 保证了 我们的start 加 size的整数倍 可以等于  end方便我们切分内存  组织成自由链表。

有同学会问 内存碎片咋办  其实没关系   因为这里浪费的内存肯定小于size大小  而且我们不是真的把内存拿走了 ,他还是在这个位置  ,最后释放的时候 ,只要给了  这块大内存的首地址,依旧是全部归还给操作系统的,不存在内存浪费的问题

 while ((void *)((size_t)start + size) != end)                                          //这里移出去的碎片 不用管   反正它依旧在这里 后面合并成大的也 直接就还给OS了
        {
            (*(void **)start) = (void *)((size_t)start + size);
            start = (void *)((size_t)start + size);
        }
        (*(void **)start) = nullptr;

然后就是 切内存了  每次+size 这样切下去    这里用!=是一样的话 我们反正上面保证了 end的合理性    如果没有上面的处理end那一步,这里大概率死循环或者 越界访问!!!包出问题的

这一步不能少 ,我们要始终保证自由链表的最后一个块指向的是空,这样才能标识结尾。

然后就是上锁,把我们切好的span给push进我们的桶  然后我写了个函数重载,因为现在我们有内存了,可以给thread_cache 分配了

这个函数是包成功的  因为我们是push之后掉的这个函数,而且是有锁的情况进来的

这里就是直接复制上面的一样的逻辑

这里纯属多余 不可能走到这里。不过是为了 消警告 因为返回值在函数里面

基本上走到这里,我们的central_cache 就全部结束了 

这三层都是大佬的设计,尤其是 span  页   大块内存 

管理的是多么的巧妙,  引入页来更方便的让我们知道任意一个span的地址范围   从而方便我们进行切分。

page_cache设计:

 见下一篇博客项目:高并发内存池(tcmalloc轻量版)——3-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值