利用二叉树的思想来实现分配和释放内存方法

利用二叉树的思想来实现分配和释放内存方法
      虽然大部分系统都有提供内存动态分配和释放函数(即C语言中的malloc和free函数),但是在嵌入式开发中由于系统的限制往往需要自己来实现内存管理,如在有些平台上可动态申请的最大空间不能满足程序设计的需要,有些系统提供的内存分配和释放函数会造成大量的内存碎片导致内存不够用,在这些时候往往就需要自己先申请一块较大的内存,然后在这个较大的内存中进行重新分配,即做一套独立的内存管理程序。其实自己设计一个内存管理程序有如下几个好处:
1、便于对程序在运行中对内存的使用情况进行监测,如是否有内存泄漏,内存使用的峰值等;
2、防止因程序设计不当(如指针越界、野指针)造成对系统的破坏,便于界定问题出现的范围;
3、通过优化内存管理算法可以自行减少内存碎片;
4、便于程序移植,在程序移植到不同平台时可以不用考虑系统的内存管理情况,只须考虑能否得到大块内存即可。
关于内存的分配和释放方法也许有很多方法可以采用,这里提出一个利用二叉树的思想来管理内存。二叉树是一种常用的数据结构,被运用到很多种算法中,二叉树的定义在这里就不再做赘述了,其从形态上可以看出二叉树主要特点是:二叉树中每一个结点最多有两个子结点且最多有一个父结点。内存的分配即是对一个较大的内存块分割成几个较小的内存块,一般采用的方法是以2的幂次作为块的大小(即内存粒度),当要分配的内存大小不等于所设定的内存粒度时则找一个最靠近这个大小且比它大的粒度来分配,这种分配方法完全可以用一个二叉树的形式来表示,如下图:
上图是从一个1KB的内存空间中分配出64字节的情况,叶子结点表示被分割出来的内存块,当要查找一块空闲的内存块时,应当是在这些叶子结点中查找,如果找不到,则找一个较大一点的空闲结点进行再分配。
运用二叉树的思想主要目的是用于理解空闲空间的回收方法,内存的不断分配和释放势必会造成大量的内存碎片,如果不对内存碎片进行整理最终会导致无内存可用,所以在释放(free)内存时要把连续的内存空间合并起来,以免有内存碎片,但并不是所有的连续空间都适宜合并,如下图:
内存块h、i、j、k为连续空间,其中h和k空间为已被使用的空间,i和j空间是刚被释放出来的空闲空间,显然,空闲空间i和j可以组成一个更大的如128B大小的空闲空间,但是这样做的结果是不仅破坏了二叉树原有的思想也造成以后若要合并空间h和k会比较复杂,会增加很多算法,如要判断空间i和j是否已合并,而且每释放一个内存空间都要对每层的二叉树进行这种类似的判断,虽然这样可以更加减少内存碎片,但是释放内存函数是个常用的函数,这种计算量特别是在嵌入式开发中是无法承受的,而且出现上面的这种情况在程序中一般较少出现,所以我们对于这样的连续空间不进行合并,只有对是同一个父结点的两个叶子结点才进行合并。
总的思想是: 对内存进行切割时,总是把一块大的内存空间切割成两块小的内存空间,对内存进行合并时,只把原来都是由同一个内存块分割出来的两块内存块进行合并
下面就根据这样的思想来实现内存申请和释放方法:
1.  数据结构定义
用一个数组定义内存粒度(以2的倍数来定义):
#define GRANULARITY_NUMBER 12    
const int MemGranularity [GRANULARITY_NUMBER] =
                            {4,8,16,32,64,128,256,512,1024,2048,4096,0xffff};
       建立一个以粒度来分类的内存块指针数组(内存桶):
              typedef struct mem_block_info_struct
              {
                     void* memptr;                    //内存块指针
                     mem_block_info_struct* nextblock;//指向后一个内存块的指针
              }MemBlockInfo;
              typedef struct mem_array_head_struct
              {
                     MemBlockInfo* freedmemarray;//指向空闲内存块链表的指针
                     MemBlockInfo* usedmemarray;//指向已用内存块链表的指针
              }MemArrayHead;
 
              MemArrayHead MemBucket[GRANULARITY_NUMBER] ={ };
    如MemBucket [0]是指向内存块大小为4字节的内存块链表,MemBucket [1]则是指向内存块大小为8字节的内存块链表,依此类推。
          
              可以在大块内存前面划出一部分内存来存储每个内存块的信息,如下图:

 这里就必须要预先估计要划分出多大的内存来存储每个内存块的信息,对于较小的运行程序比较容易估计,但是如果程序较复杂则要通过实验来确定划分大小,可以事先设定一个宏定义值,以后只要改这个宏定义值就可以了,如:

       #define MEMORY_BLOCK_MAX_NUMBER 2048
       MemBlockInfo* MemBlockArray = NULL;
    MemBucket指向的内存块链表都按内存块指针(memptr)值进行由低到高的排序,其实只要在插入一个新内存块时按指针值大小有序插入就可以了,这样做以便于查找链表中的结点,以提高查找效率。
2.  算法实现
1)、 初始化方法
   a. 初始化全局变量;
  b. 在内存块前部分出一部分用来存储内存块信息;
c. 从剩余的内存空间开始按设定的内存粒度对内存进行切割,切割原则是使得切割出来的内存块总数为最小值,方法是按粒度由大到小进行切割,假设有剩余空间大小为7680B,则可切割出内存块有:4096B、2048B、1024B、512B(这意味着我们先建立4个只有一个结点的二叉树),这些内存块的指针存到MemBlockArray中,然后根据粒度大小和内存块大小把MemBucket相应指针指到MemBlockArray中。
     2 )、动态分配内存方法(即 malloc 函数实现方法)
                a. 根据传入参数中指定的大小来决定要分配多大的内存粒度,当分配大小刚好等于某个粒度大小时,则按该粒度大小来分配,如果找不到相应大小粒度,则找一个比要分配的空间大小要大的最小粒度作为分配空间,例如要分配20字节空间,则分配一个粒度为32字节的空间。
                     b. 根据粒度大小在空闲链表中寻找相应空闲空间:若找到则返回该空闲空间指针,并把相应结点移入到已用链表中;若没有找到,则找一个粒度更大一点的空闲块,然后把该空闲块进行一半一半地分割,直到分割出得到所要的粒度大小为止,最后把被分割的空闲块从链表中移出,并把该结点的指针都置为NULL,分割出来的空闲块指针都存到MemBlockArray中,MemBlockArray中新增的数组单元内容作为结点添加到MemBucket空闲链表中(按指针大小顺序来添加),取分割出来的其中一个最小的空闲块作为要返回的内存块,并把这个内存块移入到已用链表中。
            3)、释放内存块方法(即free函数实现方法)
              搜索所有的已用链表,找出与传入的指针大小一样的结点,把该结点从已用链表中移出到空闲链表中,也是按顺序插入到空闲链表中,如果发现相邻结点为连续空间,而且相邻结点都原来为从同一个较大的空闲内存块分割出来的(即为同一个父结点的两个子结点),则合并这两个内存块,把合并后的内存块移入到更大粒度的空闲链表中,如果在更大的粒度空闲链表中又发现有可合并内存块,则再进行合并,依此方法,直到不能合并为止。
                     合并空闲内存块方法:把这两个内存块结点都从空闲链表中移出,按内存地址大小,把较小地址指针的结点移到更大粒度的空闲链表中,把较大地址指针的结点中所有指针值都置为NULL。
                     判断两个内存块是否是从同一个较大的空闲内存块分割出来的方法:如下图
   如果内存块1和内存块2都是从同一个较大的空闲内存块分割出来的,则满足如下条件:
              (前面内存块+内存块1)/内存块1的大小:这个结果为奇数
 (前面内存块+内存块1+内存块2)/内存块2的大小:这个结果为偶数
3.该方法优缺点
1)、优点:实现方法较简单,产生的内存碎片较少。
2)、缺点:内存空间利用率不高,当内存分配中需要较多内存块时,由于要存储内存块信息,要浪费较大的内存空间,而且存储内存块信息的数组长度为固定大小,则在具体应用中可能需要调节该大小,较不灵活。
 
 

你可能感兴趣的:(程序设计,struct,数据结构,存储,算法,嵌入式,null)