【手写数据库内核组件】0301 动态内存池,频繁malloc/free让系统不堪重负,动态内存池让应用自由使用动态内存

news2024/11/11 22:18:30

动态内存管理

专栏内容

  • postgresql使用入门基础
  • 手写数据库toadb
  • 并发编程

个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

文章目录

  • 动态内存管理
  • 一、概述
  • 二、动态内存管理原理
    • 2.1 内存分配策略
    • 2.2 内存回收策略
  • 三、动态内存管理实现
    • 3.1 内存管理结构定义
    • 3.2 memPage 申请与释放
    • 3.3 memNode 释放与释放
    • 3.4 应用申请内存
  • 四、总结
  • 结尾

一、概述


在平时运行时分配内存时,使用动态内存分配malloc/free或 new/delete,会直接从操作系统申请虚拟内存,释放时也归还给操作系统;

而操作系统会记录那些内存片段正在使用,那些已经释放;时间久后,内存碎片就越来越多,最终没法分配一片较大的内存出来,所以系统会尽量让空闲的片段连成一整片,不断的整理。

一般应用程序需要动态分配的次数和频率不高时,对操作系统的负担不重;

而像数据库这样的重型应用跑起来,会有大量的运行时动态内存的分配和释放,如果不对动态内存进行用户态的管理,必然会拖慢操作系统的运行,影响所有应用的性能。

本文就来分享一种在应用程序中进行动态内存管理的方法,减少向操作系统申请和释放的次数,来避免内存碎片整理的负担。

二、动态内存管理原理


动态内存管理的目标主要有两个:

  • 减少向操作系统申请和释放的次数,也就是减少调用malloc/free;
  • 减少内存碎片,一旦产生碎片就需整理,同时会造成一部分内存的浪费;

2.1 内存分配策略

针对这两个目标,我们可以这样来做,先从内存的分配来看:

  1. 每次从操作系统申请一块较大的内存;单次数量多,次数就会少很多;
  2. 应用需要时,从这块大内存中切割划分,直到大块内存用尽,再重复1步骤;
  3. 对于少量一次需要很大的内存时,超过每次申请的上限,此时就直接从操作系统申请,这就按特例来处理。

这有点像买蛋糕,当一个人吃时,每次就买一小块;当过生日时,有很多人一起吃,那就一次性买个大蛋糕,回来切分。

我们也可以说算法来源于生活,而高于生活!

2.2 内存回收策略

对于内存的释放回收,有两种策略:

  • 重复利用策略,释放时不归还给操作系统;下次申请时再重复利用;
  • 整体归还策略, 释放时不归还操作系统,如果一大块都释放时,则将此整块内存统一归还给操作系统;

重复利用策略

这种策略对于内存利用次数较高,与操作系统交互更少,但需要增加内存碎片的管理,也就是对于应用使用的不同大小的内存片,归还后要么进行移动合并,要么按大小进行排序再利用;

会增加碎片管理的负担,同时内存会有一些浪费。

整体归还策略

申请一大块内存,在应用内部进行切分为更小的片进行使用,当此大块内存上的分片均释放时,将此大块内存归还给操作系统。

这样避免内存碎片的整理,同时大块的释放也会减少操作系统内存碎片整理的负担;

但是也会存在内存浪费,当大块内存上有一小片一直不释放时,整块内存就会被浪费,迟迟得不到释放。

当然两种策略还可以再优化,但过多的优化,都会带来一些额外的开销。

三、动态内存管理实现


动态内存管理的基本单位称为内存页(page),大小为4KB,也就是上节提到的大块内存,每次申请与释放都会按内存页来操作。

在这里插入图片描述

  • 而对于应用程序来讲,它申请内存是从memPage中进行分配,每次分配一个memNode,包含申请的内存大小。

  • 当一个MemPage用完或不够时,再从下一个memPage中分配。

  • 所有的memPage采用单链表的形式串起来,方便释放时管理。

下面我们就分拆来了解一下,内存页的定义,以及内存的申请与释放的操作。

3.1 内存管理结构定义

内存管理需要记录mempage的链表,同时为了更少的与操作系统交互,增加了一个freeList来记录释放的memPage,当freeList不空时,直接从这里分配即可。

内存管理结构的定义

#define MEMORY_POOL_MANAGER_VERSION (0x0B10)
typedef struct MemPoolManagerContext
{
    int             version;
    unsigned long   totalSize;    /* 已经使用的动态内存大小 */

    // SPINLOCK        lock;         /* protected this structure. */
    
    DList           memFreeList;   
    DList           memPageList;      /* memPageInfo list */
    MemPageInfo     *currUsePage;
}MemPoolManagerContext;

成员说明:

  • version,模块的版本;
  • totalSize,记录当前使用了多少真实的内存空间;
  • memFreeList,空闲内存页的链表;
  • memPageList,正在使用的内存页的链表;
  • currUsePage,当前有最后一个内存页,下次分配时从此内存页上分配;

内存管页结构定义

内存页memPage作为向操作系统申请和释放的基本单位,它的定义如下:

#define MEMORY_POOL_PAGE_SIZE (4096)

#define MEMPAGE_INFO_LEN (sizeof(MemPageInfo))
typedef struct MemPageInfo
{
    DList list;
    int memPageSize;
    int useOffset;
    int freeSize;  
    int releaseSize;         
}MemPageInfo;

typedef char *MemPage, *MemPtr;

成员说明:

  • list,由双向链表来记录所有的内存页;
  • memPageSize,当前内存页的大小;可能有超大的内存页;
  • useOffset,使用内存的偏移;
  • freeSize,空闲内存大小;
  • releaseSize, 释放内存大小;记录当前内存页上使用过后,释放的内存大小,当全部释放后,此内存页也会释放;

这里新定义了一定类型MemPage, 也就是char *的别名,这样在后面分配内存页时使用,方便区分。

内存管节点结构定义

应用程序每次申请内存,都会分配一个内存节点结构,包含了申请的内存大小。

/* 
 * MemAlloc will alloc a MemBlock, and return MemBlock->ptr for user.
 */
typedef struct MemBlock 
{
    int size;                   /* memblock size + ptr[] size */
    MemPageInfo *memPage;       /* current memPageInfo pointer */
    char ptr[];
}MemBlock;

成员说明:

  • size , 记录当前内存节点的大小;
  • memPage,记录当前内存节点所属的内存块,指向内存页结构;
  • ptr, 返回给应用程序的内存首地址,这里没有定义大小;在低版本编译器中可能不支持,可以指定数组大小为1;

注意,这里ptr必须定义为数组,因为它就是数据的首地址,也就是一段内存的开始;如果定义为指针,需要手动赋值;

3.2 memPage 申请与释放

内存页的分配

当MemPage不够分配时,都需要从操作系统中申请一块新的memPage。

先从FreeList中查找,如果找到,就添加到当前使用列表中;

如果没有空闲memPage时,直接调用malloc进行分配。

static int AddNewPageToContext(MemPoolManagerContext *poolContext)
{
    MemPageInfo *currentMemPage = NULL;
    MemPage newPage = NULL;

    newPage = GetFreeMemPage();
    if(NULL == newPage)
    {
        newPage = (MemPage)malloc(MEMORY_POOL_PAGE_SIZE);
        if(NULL == newPage)
        {
            exit(-1);
        }
    }        

    currentMemPage = InitMemPage(newPage, MEMORY_POOL_PAGE_SIZE);
        
    /* add to context, and continue to alloc from context. */
    AddMemPageToContext(currentMemPage, poolContext);      

    return 0;
}

添加到使用列表中,同时将使用中的内存总数进行累加。

static int AddMemPageToContext(MemPageInfo *memPage, MemPoolManagerContext *context)
{
    context->totalSize += memPage->memPageSize;       
    
    /* add to mempage list */
    AddMemPageNode(&(context->memPageList), memPage);   
    context->currUsePage = memPage;
    
    return 0;
}

其中AddMemPageNode是将新的内存页加到链表中,这个在链表章节介绍。

3.3 memNode 释放与释放

每次应用申请内存,都是从内存池中分配一个memNode。

当申请的size 小于当前内存页的空闲空间时,在当前页中分配一个memBlock结构;

static MemPtr AllocFromMemPage(MemPageInfo *mPage, int size)
{
    MemBlock *memb = NULL;

    mPage->freeSize -= size;
    memb = (MemBlock*)((char *)mPage + mPage->useOffset);
    mPage->useOffset += size;

    memb->memPage = mPage;
    memb->size = size;

    return (MemPtr)(memb->ptr);
}

而当应用程序释放内存时,会将memBlock归还给对应的memPage;

在分配时,需要记录对应的memPage的地址,此时就可以进行引用。

static int ReleaseToMemPage(MemBlock *memb)
{
    MemPageListInfo *memPageList = NULL;
    MemPageInfo *memPage = memb->memPage;
    DList *header = NULL;
    int ret = 0;

    if(NULL == memPage)
    {
        return -1;
    }

    memPage->releaseSize += memb->size;

    return 0;
}

释放时只是将待释放的内存大小增加到了 releaseSize;

当然这里可以将再进行优化,当memPage为空时,将它从使用列表中移除,并填加到空闲列表中。

3.4 应用申请内存

应用程序申请内存时,不能再使用malloc/free这两个接口了,需要使用自定义的接口。

在这里插入图片描述

动态内存申请接口

申请的流程如下:

  • 需要先定义一个全局的MemPoolManagerContext结构,来保存整个申请的内存页列表;通过GetMemPoolCurrentContext,可以达到单例获取的目的。
  • 申请的内存size需要增加MemBlock的头部成员的大小;
  • 当申请的size大于内存页可分配的上限时,直接分配一个非标准的内存页,将将它加到内存管理链表中;
  • 然后从内存管理中进行分配内存节点;
#define MEMORY_POOL_BLOCK_HEADER_SIZE (sizeof(MemBlock))
#define MEMPAGE_INFO_LEN (sizeof(MemPageInfo))
#define MEMORY_POOL_MAX_ALLOC_SIZE (MEMORY_POOL_PAGE_SIZE - MEMPAGE_INFO_LEN)

MemPtr AllocFromMemPool(unsigned int size)
{
    MemPtr mem = NULL;
    MemPoolManagerContext *currentMemContext = NULL;
    MemPageInfo *currentMemPage = NULL;
    MemPage newPage = NULL;

    currentMemContext = GetMemPoolCurrentContext();
    if(NULL == currentMemContext)
    {
        return NULL;
    }

    /* add memblock header size */
    size += MEMORY_POOL_BLOCK_HEADER_SIZE;

    /* oversize */
    if(size >= MEMORY_POOL_MAX_ALLOC_SIZE)
    {
        newPage = (MemPage)malloc(size);

        currentMemPage = InitMemPage(newPage, size);
        AddMemPageToContext(currentMemPage, currentMemContext); 

        mem = AllocFromMemPage(currentMemPage, size);
        return mem;
    }

    do {
        mem = AllocFromMemContext(size, currentMemContext);
        if(NULL != mem)
            break;

        AddNewPageToContext(currentMemContext);     
    }while(1);

    return mem;
}

这里有一个小技巧,后面使用了一个do { }while(1)的循环,其实这里就是为了写法简单,当没有空闲空间时,会申请一个内存页,然后继续分配内存节点。

动态内存释放接口

这里相对简单,就是将当前内存节点释放到对应的内存页上。


#define GetOffsetSize(type, member) (unsigned long)(&(((type *)(0))->member))
#define GetAddrByMember(memberaddr, member, type) (type *)(((char*)(memberaddr)) - GetOffsetSize(type,member))
#define GetMemBlockHeader(memptr) (GetAddrByMember(memptr, ptr, MemBlock))
int ReleaseToMemPool(MemPtr mem)
{
    return ReleaseToMemPage(GetMemBlockHeader(mem));
}

在应用程序中拿到的是ptr数组首地址,通过它在结构体中的偏移,可以找到memBlock结构的地址,这里定义了一个宏。

当然可以使用offsetof,这个预定义的函数来获取结构体成员的偏移。

四、总结


在动态内存使用频繁的应用程序中,不仅与操作系统交互多,而且会造成大量的内存碎片,增加额外的系统负担。

本文分享了通过动态内存池的方法,每次申请一个内存页,然后在当有动态内存需要时,进行切分,可以避够内存碎片的产生。

当然也存在很多可优化的地方,在释放时可以保留一部分内存页在freeList中,这样进一步减少与操作系统的交互。

结尾


非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!

作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。

注:未经同意,不得转载!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1922795.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI算法07-人工神经网络Artificial Neural Network | ANN

什么是神经网络 简介 人工神经网络(ANN)或连接系统是由构成动物大脑的生物神经网络模糊地启发的计算系统。神经网络本身不是算法,而是许多不同机器学习算法的框架,它们协同工作并处理复杂的数据输入。此类系统通过考虑示例“学习…

基于颜色模型和边缘检测的火焰识别FPGA实现,包含testbench和matlab验证程序

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 将FPGA仿真结果导入到matlab显示结果: 测试样本1 测试样本2 测试样本3 2.算法运行软件版本 vivado2019.2 …

车载视频监控管理方案:无人驾驶出租车安全出行的保障

近日,无人驾驶出租车“萝卜快跑”在武汉开放载人测试成为热门话题。随着科技的飞速发展,无人驾驶技术已逐渐从概念走向现实,特别是在出租车行业中,无人驾驶出租车的推出将为公众提供更为安全、便捷、高效的出行服务。 视频监控技…

IntelliJ IDEA自定义菜单(Menus)、任务栏(toolbars)详细教程

本示例是基于IDEA2024.1Ultimate版本的New UI模式下 一、自定义菜单 1、打开Settings,找到Menus and Toolbars 2、点击右边的Main Menu,点击号,选择Add Action 3、弹出Add Action弹窗,搜索或者选择你要添加的指令 二、自定义工具…

东软医疗 踩在中国医疗科技跃迁的风口上

恐怕没有哪一家本土医疗装备企业能像东软医疗一样,每一段成长的升维都发生在中国医疗科技跃迁史最重要的节点上。 在工业制造领域,医疗装备产业由于涉及数十个学科领域,其技术复合程度毫不逊于今天公众所熟知的EUV光刻机,是一门技…

java基础之接口

接口和抽象类很像,接口是把行为给抽象化,可以理解成一个抽象类抽象到极致的情况下,形成的类,也就是一个抽象类有且只有抽象方法的时候,就可以用接口来写。 一、抽象类与接口在书写上的异同 这是一个抽象类 public abst…

jmeter-beanshell学习8-for循环

一个稍微有点难度的东西 要把响应结果的所有名字都取出来,然后怎么处理看自己需求。比如找某个人是不是在这里,或者把所有人都写进一个文档,我就不编场景了 第一步想要取出所有名字,还得靠万能的正则表达式提取器,jso…

零信任的架构结合模块化沙箱,实现一机两用的解决方案

零信任沙箱是深信达提出的一种数据安全解决方案,它将零信任原则与SDC沙箱技术的优势相结合。零信任原则是一种安全概念,核心思想是“永不信任,总是验证”。它要求对每一个访问请求都进行严格的身份验证和授权,无论请求来源于内部还…

Androidstudio安卓开发,SharedPreferences实现登录注册

1. 项目涉及到的技术点 SharedPreferences的使用 2. 效果图 3. 实现过程 注册布局文件&#xff1a;activity_register.xml <?xml version"1.0" encoding"utf-8"?> <androidx.appcompat.widget.LinearLayoutCompat xmlns:android"http:…

Python 爬虫:使用打码平台来识别各种验证码:

本课程使用的是 超级鹰 打码平台&#xff0c; 没有账户的请自行注册&#xff01; 超级鹰验证码识别-专业的验证码云端识别服务,让验证码识别更快速、更准确、更强大 使用打码平台来攻破验证码难题&#xff0c; 是很简单容易的&#xff0c; 但是要钱&#xff01; 案例代码及测…

【C语言】实践:贪吃蛇小游戏(附源码)

欢迎光顾我的homepage 前言 贪吃蛇小游戏想必大家都玩过吧&#xff0c;现在就要C语言代码来实现一下贪吃蛇小游戏 在实现之前&#xff0c;我们要对C语言结构体、指针、链表(单链表)有一定的基础 先来看一下预期运行效果 一、Win32 API 这里实现贪吃蛇游戏会使用一些Win32 AP…

7.8~7.10练习

目录 1.扑克牌游戏 2.链表基本功能的实现&#xff08;单项链表&#xff09; 3.移除链表元素力扣 4.反转链表力扣 5.链表的中间结点 5.返回倒数第k个节点​编辑 6.合并两个有序链表 7.链表基本功能的实现&#xff08;双向链表&#xff09; 8.链表分割 1.扑克牌游戏 public…

新手教学系列——高效管理MongoDB数据:批量插入与更新的实战技巧

前言 在日常开发中,MongoDB作为一种灵活高效的NoSQL数据库,深受开发者喜爱。然而,如何高效地进行数据的批量插入和更新,却常常让人头疼。今天,我们将一起探讨如何使用MongoDB的bulk_write方法,简化我们的数据管理流程,让代码更加简洁高效。 常规做法:find、insertone…

LabVIEW扬尘控制系统

设计了一套基于LabVIEW的扬尘控制系统&#xff0c;通过监测TsP&#xff08;总悬浮颗粒物&#xff09;浓度、风向和摄像头视频&#xff0c;实现对环境的综合监控和扬尘控制。系统可以自动判断扬尘位置&#xff0c;并驱动抑尘设备进行抑尘。硬件选用NI cDAQ-9178数据采集模块、Om…

9.5 栅格图层符号化多波段彩色渲染

文章目录 前言多波段彩色渲染QGis设置为多波段彩色二次开发代码实现多波段彩色 总结 前言 介绍栅格图层数据渲染之多波段彩色渲染说明&#xff1a;文章中的示例代码均来自开源项目qgis_cpp_api_apps 多波段彩色渲染 以“3420C_2010_327_RGB_LATLNG.tif”数据为例&#xff0c…

26.7 Django单表操作

1. 模型管理器 1.1 Manager管理器 Django ORM中, 每个Django模型(Model)至少有一个管理器, 默认的管理器名称为objects. objects是一个非常重要的管理器(Manager)实例, 它提供了与数据库进行交互的接口.通过管理器, 可以执行数据库查询, 保存对象到数据库等操作.objects管理器…

MT6825磁编码IC在智能双旋机器人中的应用

MT6825磁编码IC在智能双旋机器人中的应用&#xff0c;无疑为这一领域的创新和发展注入了新的活力。作为一款高性能的磁性位置传感器&#xff0c;MT6825以其独特的优势&#xff0c;在智能双旋机器人的运动控制、定位精度以及系统稳定性等方面发挥了关键作用。 www.abitions.com …

【Web开发手礼】探索Web开发的魅力(三)-html基础标签(3)

上述主要是对html标签的介绍和一些基本练习可以当作日常笔记收藏一下&#xff01;&#xff01;&#xff01; 目录 前言 html基础标签 前言 上述主要是对html标签的介绍和一些基本练习可以当作日常笔记收藏一下&#xff01;&#xff01;&#xff01; 提示&#xff1a;以下是本…

Kithara与OpenCV (二)

Kithara使用OpenCV QT 进行特征检测 目录 Kithara使用OpenCV QT 进行特征检测OpenCV 特征检测简介Qt应用框架简介项目说明关键代码抖动测试测试平台&#xff1a;测试结果&#xff1a;结论 OpenCV 特征检测简介 OpenCV是一个开源的计算机视觉库&#xff0c;提供了各种图像处理…

WordPress 主题技巧:给文章页增加“谁来过”模块。

模块功能&#xff1a; 我个人目前在做一个电影类的网站&#xff0c;在开发文章页的模版时候&#xff0c;突然觉得给文章页增加一个“谁对本电影感兴趣”的功能模块可能会比较有趣&#xff0c;这个功能有点类似于‘足迹’的感觉&#xff0c;用户可以通过这个功能&#xff0c;发…