0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

详解内存池技术的原理与实现

Linux内核补给站 来源:Linux内核补给站 作者:Linux内核补给站 2022-05-20 08:58 次阅读

序言

最近在网上看到了几篇篇讲述内存池技术的文章,有一篇是有IBM中国研发中心的人写的,写的不错~~文章地址在本篇blog最后。原文的讲述比我的要清晰很多,我在这只是把我的一些理解和遇到的一些问题和大家分享一下~~

一、为什么要使用内存池技术呢

主要有两个原因:1、减少new、delete次数,减少运行时间;2、避免内存碎片。

1、效率

c语言中使用malloc/free来分配内存,c++中使用new/delete来分配内存,他们的内存申请与释放都是与操作系统进行交互的。具体的内容在严蔚敏数据结构的第八章有相关讲述,主要就是系统要维护一个内存链表,当有一个内存申请过来时,根据相应的分配算法在链表中找个一个合适的内存分配给它。这些算法有的是分配最先找到的不小于申请内存的内存块,有的是分配最大的内存块,有的是分配最接近申请内存大小的内存块。分配的内存块可能会大于所申请的内存大小,这样还有进行切割,将剩余的内存插入到空闲链表中。当释放的时候,系统可能要对内存进行整理,判断free的内存块的前后是否有空闲,若有的话还要进行合并。此外,new/delete还要考虑多线程的情况。总之一句话,调用库中的内存分配函数,十分的耗时~~

2、内存碎片

什么是内存碎片内,从字面意思就很好理解了,就是内存不再是一整块的了,而是碎了。因为连续的这种new/delete操作,一大块内存肯能就被分割成小的内存分配出去了,这些小的内存都是不连续的。当你再去分配大的连续内存的时候,尽管剩余内存的总和可能大于所要分配的内存大小,但系统就找不到连续的内存了,所以导致分配错误。malloc的时候会导致返回NULL,而new的时候再vc6.0中返回NULL,vs2003以上则是抛出异常。

二、原理

要解决上述两个问题,最好的方法就是内存池技术。具体方法就是大小固定、提前申请、重复利用。

因为内存的申请和释放是很低效的,所以我们只在开始时申请一块大的内存(在该块内存不够用时在二次分配),然后每次需要时都从这块内存中取出,并标记下这块内存被用了,释放时标记此内存被释放了。释放时,并不真的把内存释放给操作系统,只要在一大块内存都空闲的时候,才释放给操作系统。这样,就减少了new/delete的操作次数,从而提高了效率。

在调用内存分配函数的时候,大部分时间所分配的内存大小都是一定的,所以可以采用每次都分配固定大小的内存块,这样就避免了内存碎片产生的可能。

三、具体实现

我所采用的内存池的构造方法完全是按照文章1所介绍的方法,内存池的结构图如下:

poYBAGKGOTmAYqZTAAA5idptWh8137.gif?source=d16d100b


如图所示MemoryPool是一个内存池类,其中pBlock是一个指向了一个内存块的指针,nUintSzie是分配单元的大小,nInitSize是第一次分配时向系统申请的内存的大小,nGrouSize是后面每次向系统申请的内存的大小。

MemoryBloc代表一个内存块单元,它有两部分构成,一部分时MemoryBlock类的大小,另一部分则是实际的内存部分。一个MemoryBlock的内存是在重载的new操作符中分配的,如下所示:

void* MemoryBlock::operator new(size_t, int nUnitSize,int nUnitAmount )
{
    return ::operator new( sizeof(MemoryBlock) + nUnitSize * nUnitAmount );
}

MemoryBlock内中,nSize代码该内存块的大小(系统分配内存大小-MemoryBlock类的大小),nFree是空闲内存单元的个数,nFirst代表的是下一个要分配的内存单元的序号。aData是用来记录待分配内存的位置的。因为要分配的内存是在new中一起向系统申请的,并没有一个指针指向这块内存的位置,但它的位置就在MemoryBlock这个类的地址开始的,所以可以用MemoryBlock的最后一个成员的位置来表示待分配内存的位置。

带分配内存中,是以nUnitSize为单位的,一个内存单元的头两个字节都记录了下一个要分配的内存单元的序号,序号从0开始。这样实际也就构成了一个数组链表。由MemoryBlock的构造函数来完成这个链表的初始化工作:

MemoryBlock::MemoryBlock( int nUnitSize,int nUnitAmount )
    :   nSize   (nUnitAmount * nUnitSize),
        nFree   (nUnitAmount - 1),  //构造的时候,就已将第一个单元分配出去了,所以减一
        nFirst  (1),                //同上
        pNext   (NULL)
{
    //初始化数组链表,将每个分配单元的下一个分配单元的序号写在当前单元的前两个字节中
    char* pData = aData;
    //最后一个位置不用写入
    for( int i = 1; i < nSize - 1; i++)
    {
        (*(USHORT*)pData) = i;
        pData += nUnitSize;
    }
}

在MemoryPool的Alloc()中,遍历block链表,找到nFree大于0的block,从其上分配内存单元。然后将nFree减一,修改nFirst的值。

在MemoryPool的Free(pFree)函数中,根据pFree的值,找到它所在的内存块,然后将它的序号作为nFirst的值(因为它绝对是空闲的),在pFree的头两个字节中写入原来nFirst的值。然后要判断,该block是否全部为free,方法是检测nFree * nUnitSize == nSize。若是,则向系统释放内存,若不是,则将该block放到链表的头部,因为该block上一定含有空隙的内存单元,这样可以减少分配时遍历链表所消耗的时间。

四、使用

内存池一般都是作为一个类的静态成员,或者全局变量。使用时,重载new操作符,使其到MemoryPool中去分配内存,而不是向系统申请。这样,一个类的所以对象都在一个内存池中开辟空间。


void CTest::operator delete( void* pTest )
{   
    Pool.Free(pTest);   
}
 
 
void* CTest::operator new(size_t)
{
    return (CTest*)Pool.Alloc();
}

五、代码

MemoryPool.h

#include 
#include 
 
#define  MEMPOOL_ALIGNMENT 8            //对齐长度
//内存块,每个内存块管理一大块内存,包括许多分配单元
class MemoryBlock
{
public:
                    MemoryBlock (int nUnitSize,int nUnitAmount);
                    ~MemoryBlock(){};
    static void*    operator new    (size_t,int nUnitSize,int nUnitAmount);
    static void     operator delete (void* ,int nUnitSize,int nUnitAmount){};
    static void     operator delete (void* pBlock);
 
    int             nSize;              //该内存块的大小,以字节为单位
    int             nFree;              //该内存块还有多少可分配的单元
    int             nFirst;             //当前可用单元的序号,从0开始
    MemoryBlock*    pNext;              //指向下一个内存块
    char            aData[1];           //用于标记分配单元开始的位置,分配单元从aData的位置开始
     
};
 
class MemoryPool
{
public:
                    MemoryPool (int _nUnitSize,
                                int _nGrowSize = 1024,
                                int _nInitSzie = 256);
                    ~MemoryPool();
    void*           Alloc();
    void            Free(void* pFree);
 
private:
    int             nInitSize;          //初始大小
    int             nGrowSize;          //增长大小
    int             nUnitSize;          //分配单元大小
    MemoryBlock*    pBlock;             //内存块链表
};

MemoryPool.cpp

#include "MemoryPool.h"
 
MemoryBlock::MemoryBlock( int nUnitSize,int nUnitAmount )
    :   nSize   (nUnitAmount * nUnitSize),
        nFree   (nUnitAmount - 1),  //构造的时候,就已将第一个单元分配出去了,所以减一
        nFirst  (1),                //同上
        pNext   (NULL)
{
    //初始化数组链表,将每个分配单元的下一个分配单元的序号写在当前单元的前两个字节中
    char* pData = aData;
    //最后一个位置不用写入
    for( int i = 1; i < nSize - 1; i++)
    {
        (*(USHORT*)pData) = i;
        pData += nUnitSize;
    }
}
 
void* MemoryBlock::operator new(size_t, int nUnitSize,int nUnitAmount )
{
    return ::operator new( sizeof(MemoryBlock) + nUnitSize * nUnitAmount );
}
 
void MemoryBlock::operator delete( void* pBlock)
{
    ::operator delete(pBlock);
}
 
MemoryPool::MemoryPool( int _nUnitSize, int _nGrowSize /*= 1024*/, int _nInitSzie /*= 256*/ )
{
    nInitSize = _nInitSzie;
    nGrowSize = _nGrowSize;
    pBlock = NULL;
    if(_nUnitSize > 4)
        nUnitSize = (_nUnitSize + (MEMPOOL_ALIGNMENT - 1)) & ~(MEMPOOL_ALIGNMENT - 1);
    else if( _nUnitSize < 2)
        nUnitSize = 2;
    else
        nUnitSize = 4;
}
 
MemoryPool::~MemoryPool()
{
    MemoryBlock* pMyBlock = pBlock;
    while( pMyBlock != NULL)
    {
        pMyBlock = pMyBlock->pNext;
        delete(pMyBlock);
    }
}
 
void* MemoryPool::Alloc()
{
    if( NULL == pBlock)
    {
        //首次生成MemoryBlock,new带参数,new了一个MemoryBlock类
        pBlock = (MemoryBlock*)new(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);
        return (void*)pBlock->aData;
    }
 
    //找到符合条件的内存块
    MemoryBlock* pMyBlock = pBlock;
    while( pMyBlock != NULL && 0 == pMyBlock->nFree )
        pMyBlock = pMyBlock->pNext;
 
    if( pMyBlock != NULL)
    {
        //找到了,进行分配
        char* pFree = pMyBlock->aData + pMyBlock->nFirst * nUnitSize;
        pMyBlock->nFirst = *((USHORT*)pFree);
        pMyBlock->nFree--;
 
        return (void*)pFree;
    }
    else
    {
        //没有找到,说明原来的内存块都满了,要再次分配
 
        if( 0 == nGrowSize)
            return NULL;
         
        pMyBlock = (MemoryBlock*)new(nUnitSize,nGrowSize) MemoryBlock(nUnitSize,nGrowSize);
 
        if( NULL == pMyBlock)
            return NULL;
 
        //进行一次插入操作
        pMyBlock->pNext = pBlock;
        pBlock = pMyBlock;
 
        return (void*)pMyBlock->aData;
    }
}
 
void MemoryPool::Free( void* pFree )
{
    //找到p所在的内存块
    MemoryBlock* pMyBlock = pBlock;
    MemoryBlock* PreBlock = NULL;
    while ( pMyBlock != NULL && ( pBlock->aData > pFree || pMyBlock->aData + pMyBlock->nSize))
    {
        PreBlock = pMyBlock;
        pMyBlock = pMyBlock->pNext;
    }
 
    if( NULL != pMyBlock )      //该内存在本内存池中pMyBlock所指向的内存块中
    {      
        //Step1 修改数组链表
        *((USHORT*)pFree) = pMyBlock->nFirst;
        pMyBlock->nFirst  = (USHORT)((ULONG)pFree - (ULONG)pMyBlock->aData) / nUnitSize;
        pMyBlock->nFree++;
 
        //Step2 判断是否需要向OS释放内存
        if( pMyBlock->nSize == pMyBlock->nFree * nUnitSize )
        {
            //在链表中删除该block
             
            delete(pMyBlock);
        }
        else
        {
            //将该block插入到队首
            PreBlock = pMyBlock->pNext;
            pMyBlock->pNext = pBlock;
            pBlock = pMyBlock;
        }
    }
}

CTest.cpp

#include 
#include "MemoryPool.h"
 
class CTest
{
public:
                CTest(){data1 = data2 = 0;};
                ~CTest(){};
    void*       operator new (size_t);
    void        operator delete(void* pTest);
public:
 
    static      MemoryPool Pool;
    int         data1;
    int         data2;
};
 
void CTest::operator delete( void* pTest )
{  
    Pool.Free(pTest);  
}
 
 
void* CTest::operator new(size_t)
{
    return (CTest*)Pool.Alloc();
}
 
MemoryPool CTest::Pool(sizeof(CTest));
 
int main()
{
    CTest* pTest = new CTest;
    printf("%d",pTest->data2);
}

六、问题

在编写代码时,遇到了一些小问题,现与大家分享如下:

重载new操作符时,编译器要求是第一个参数必须是size_t,返回值必须是void*;free的第一个参数必须是void*.

一般要在类的成员中重载new操作符,而不要重载全局的new操作符。

一个类中要是重载了一个new操作符,一定要有一个相应类型的delete操作符,可以什么都不干,但必须有,否则在构造函数失败时,找不到对应的delete函数。

例如:

static void*    operator new    (size_t,int nUnitSize,int nUnitAmount);
    static void     operator delete (void* ,int nUnitSize,int nUnitAmount){};

4. 带参数的new操作符


pBlock = (MemoryBlock*)new(nUnitSize,nInitSize) MemoryBlock(nUnitSize,nUnitSize);

第一个nUnitSize nInitSize是new操作符的参数,该new操作符是new了一个MemoryBlock对象,在new返回的地址上构造MemoryBlock的对象。

5. 如果在类的内部不能进行静态成员的定义的话,可以只在内部进行声明,在外部定义:

MemoryPool CTest::Pool(sizeof(CTest));

审核编辑:汤梓红


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11216

    浏览量

    208792
  • 内存
    +关注

    关注

    8

    文章

    2990

    浏览量

    73837
  • C语言
    +关注

    关注

    180

    文章

    7596

    浏览量

    136004
收藏 人收藏

    评论

    相关推荐

    C++内存的设计与实现

    内存技术中的一种形式。通常我们在编写程序的时候回使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次
    发表于 09-23 10:22 903次阅读

    内存可以调节内存的大小吗

    嵌入式–内存直接上代码,自己体会。嵌入式设备,一般keil提供的堆很小,一般都不使用。使用内存,自己可以调节内存大小。头文件 mallo
    发表于 12-17 07:00

    内存的概念和实现原理概述

    { //一:内存的概念和实现原理概述//malloc:内存浪费,频繁分配小块内存,则浪费更加显得明显//“
    发表于 12-17 06:44

    线程是如何实现

    线程的概念是什么?线程是如何实现的?
    发表于 02-28 06:20

    关于RT-Thread内存管理的内存简析

    这篇文章继续介绍 RT-Thread 内存管理剩下的部分——内存。为何引入内存内存堆虽然方
    发表于 04-06 17:02

    RT-Thread内存管理之内存实现分析

    了解RT-thread 的内存实现及管理。以RTT最新稳定版本4.1.0的内核为蓝本。\\include\\rtdef.h/**Base structure of Memory pool
    发表于 10-17 15:06

    Linux 内存源码浅析

    内存(Memery Pool)技术是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有
    发表于 04-02 14:32 246次阅读

    基于CXL技术的大内存化方案解析

    如果 FaceBoo k平台创建的TPP协议是正确的,那么它将有一个不同的内存分页系统,可以更好地解决由于在服务器主板之外有大量内存而带来的稍高的延迟。
    发表于 10-20 11:46 2008次阅读

    什么是内存

    1什么是内存 1.1技术 所谓“技术”,就是程序先向系统申请过量的资源,然后自己管理,
    的头像 发表于 11-08 16:26 838次阅读
    什么是<b class='flag-5'>内存</b><b class='flag-5'>池</b>

    高并发内存项目实现

    本项目实现了一个高并发内存,参考了Google的开源项目tcmalloc实现的简易版;其功能就是实现高效的多线程
    的头像 发表于 11-09 11:16 665次阅读
    高并发<b class='flag-5'>内存</b><b class='flag-5'>池</b>项目<b class='flag-5'>实现</b>

    了解连接、线程内存、异步请求

    技术 技术能够减少资源对象的创建次数,提⾼程序的响应性能,特别是在⾼并发下这种提⾼更加明显。使用
    的头像 发表于 11-09 14:44 1166次阅读
    了解连接<b class='flag-5'>池</b>、线程<b class='flag-5'>池</b>、<b class='flag-5'>内存</b><b class='flag-5'>池</b>、异步请求<b class='flag-5'>池</b>

    如何实现一个高性能内存

    写在前面 本文的内存代码是改编自Nginx的内存源码,思路几乎一样。由于Nginx源码的变量命名我不喜欢,又没有注释,看得我很难受。想自己写一版容易理解的代码。 应用场景 写
    的头像 发表于 11-10 11:11 618次阅读
    如何<b class='flag-5'>实现</b>一个高性能<b class='flag-5'>内存</b><b class='flag-5'>池</b>

    内存的使用场景

    为什么要用内存 为什么要用内存?首先,在7 * 24h的服务器中如果不使用内存,而使用ma
    的头像 发表于 11-10 17:19 664次阅读
    <b class='flag-5'>内存</b><b class='flag-5'>池</b>的使用场景

    nginx内存源码设计

    造轮子内存原因引入 作为C/C++程序员, 相较JAVA程序员的一个重大特征是我们可以直接访问内存, 自己管理内存, 这个可以说是我们的特色, 也是我们的苦楚了. java可以有虚拟
    的头像 发表于 11-13 11:51 665次阅读
    nginx<b class='flag-5'>内存</b><b class='flag-5'>池</b>源码设计

    内存主要解决的问题

    内存的定义 1.技术 是在计算机技术中经常使用的一种设计模式,其内涵在于:将程序中需要
    的头像 发表于 11-13 15:23 654次阅读
    <b class='flag-5'>内存</b><b class='flag-5'>池</b>主要解决的问题