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

    文章

    10943

    浏览量

    206546
  • 内存
    +关注

    关注

    8

    文章

    2737

    浏览量

    72615
  • C语言
    +关注

    关注

    180

    文章

    7513

    浏览量

    124252
收藏 人收藏

    评论

    相关推荐

    PWM技术实现方法原理详解

    立即学习>>>【史上最全半桥LLC谐振式开关电源视频教程】每天学习1小时 张飞带你两个月精通半桥LLC开关电源!PWM技术实现方法原理详解总结了PWM技术问世至今各种主要的
    发表于 01-10 12:14

    是什么 短信猫使用方法

    是什么 短信猫使用方法 能自动完成移动、电信、联通KPI考核服务的《伊卡通9.6》系统, 伊卡通自消费软件是围绕移动KPI指标来设计开发的一款软件产品,该款软件可配合相关的猫设备(如我们的8
    发表于 04-23 15:38

    C语言内存管理详解

    C语言内存管理详解,很不错的一份资料.
    发表于 08-06 23:14

    请问战舰LWIP移植是怎么实现内存管理的?

    如题,最近在移植LWIP,参考原子战舰V3,由于我的系统没实现内存管理,因此,涉及到malloc的函数我全部使用全局数据区来开辟空间(暂时先这么粗略地实现),但对内存
    发表于 09-02 04:36

    【每日一练】第十六节:内存的使用

    本视频为【每日一练】的第16节学习视频,注:刚开始学习的童鞋请从第一节视频开始打卡哦(本节视频在下面打卡即可)学习任务:1、删除内存时,会首先唤醒等待在该内存对象上的所有线程。(判
    发表于 09-08 09:33

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

    嵌入式–内存直接上代码,自己体会。嵌入式设备,一般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操作系统中静态内存的创建与使用

    程序运行,创建一个内存,一个申请内存任务,一个释放内存任务,u***串口CN3打印内存分配和释放的信息,串口波特率115200//创建
    发表于 05-10 14:51

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

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

    删除静态内存是用rt_mp_detach还是rt_mp_delete

    可否动态申请一块内存作为静态内存,然后再在这块静态内存进行相关的静态内存操作?删除静态
    发表于 11-22 14:42

    DDR系列内存详解及硬件设计规范l

    DDR系列内存详解及硬件设计规范, 好的教程
    发表于 11-16 18:59 0次下载

    SDRAM内存详解资料

    SDRAM内存详解资料
    发表于 10-30 15:45 8次下载
    SDRAM<b class='flag-5'>内存</b><b class='flag-5'>详解</b>资料

    详解String对象的内存分配

    详解String对象的内存分配
    的头像 发表于 07-01 10:09 2030次阅读