zoukankan      html  css  js  c++  java
  • Pooled Allocation池式分配实例——Keil 内存管理

    最近翻看Kei安装目录,无意中发现C51LIB下的几个.C文件:

    CALLOC.C
    FREE.C
    INIT_MEM.C
    MALLOC.C
    REALLOC.C

    看到 MALLOC.C 和 FREE.C 想到可能和“内存管理”有关。花了半个上午把这个几个文件看完,感觉代码虽然短,确有几个巧妙之处。看的时候也有几处疑问,看完之后豁然开朗。

    1) CALLOC.C

    我首先点开的是calloc.c(因为calloc()平时没怎么用过,最为好奇),看到了这样的代码:

       1: void _MALLOC_MEM_ *calloc (
       2:   unsigned int size,
       3:   unsigned int len)
       4: {
       5: void _MALLOC_MEM_ *p;
       6:  
       7: size *= len;
       8:  
       9: if ((p = malloc (size)) == NULL)
      10:   return (NULL);
      11:  
      12: memset (p, 0, size);
      13: return (p);
      14: }

    这个函数很简单,它并没有直接获取内存,而是调用了malloc;看到这样的代码很容易想到——这是一个用来分配动态数组的函数。size是元素大小,len是数组长度。应该是这样用的:

       1: int *pBase;
       2: // ...
       3: pBase = (int*)calloc(sizeof(int), 10); // 10个整数
       4: // ...

    在calloc里看的了 _MALLOC_MEM_ 让人不解,顺着CALLOC.C的#include找上去,看到了:

       1: #define _MALLOC_MEM_    xdata

    原来是这个… …(如果有同学不知道xdata是什么,可以简单的理解为“堆”。管它呢!)。

    2) MALLOC.C

    继续点开MALLOC.C(这份代码不短),一看到头部,猜到它可能是个链表:

       1: struct __mem__
       2:   {
       3:   struct __mem__ _MALLOC_MEM_ *next;    /* single-linked list */
       4:   unsigned int                len;     /* length of following block */
       5:   };

    很明显,这里的next用作链接;但是,len的作用暂时还不能确定(猜测:标识空闲块的长度,注释说的)。它们是这样:

    malloc_link

    接下来是类型、常量定义定义:

       1: typedef struct __mem__         __memt__;
       2: typedef __memt__ _MALLOC_MEM_ *__memp__;
       3:  
       4: #define    HLEN    (sizeof(__memt__))
       5:  
       6: extern __memt__ _MALLOC_MEM_ __mem_avail__ [];
       7:  
       8: #define AVAIL    (__mem_avail__[0])
       9:  
      10: #define MIN_BLOCK    (HLEN * 4)

    看到这些typedef,#define也不能确定各自是做什么用的。但是有个extern声明的数组!应该在别的地方有定义。(关于声明和定义不多说了)

    然后就是完整的malloc()了(部分注释已被删除):

       1: void _MALLOC_MEM_ *malloc( unsigned int size)
       2: {
       3: __memp__ q;            /* ptr to free block */
       4: __memp__ p;            /* q->next */
       5: unsigned int k;        /* space remaining in the allocated block */
       6:  
       7: q = &AVAIL;
       8:  
       9: while (1)
      10:   {
      11:   if ((p = q->next) == NULL)
      12:     {
      13:     return (NULL);                /* FAILURE */
      14:     }
      15:  
      16:   if (p->len >= size)
      17:     break;
      18:  
      19:   q = p;
      20:   }
      21:  
      22: k = p->len - size;        /* calc. remaining bytes in block */
      23:  
      24: if (k < MIN_BLOCK)        /* rem. bytes too small for new block */
      25:   {
      26:   q->next = p->next;
      27:   return (&p[1]);                /* SUCCESS */
      28:   }
      29:  
      30: k -= HLEN;
      31: p->len = k;
      32:  
      33: q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k);
      34: q->len = size;
      35:  
      36: return (&q[1]);                    /* SUCCESS */
      37: }

    稍加分析可知,while(1)是循环遍历链表的(循环内的p=q->next和q=p这两句)。所以q一开始指向的应该是链表的头结点,AVAIL即__mem_avail__[0]里存放着链表的头结点。
    由16行 if(p->len > size) break; 可知,len的作用确实是用来标识空闲块的长度;
    所以整个链表应该是这样的(绿色部分为空闲内存,白色是链表节点):
    malloc
    由此可知,注释里的free block指的是一个“白色+绿色”。

    注意,一旦满足条件(找到一个足够大的空闲块),跳出循环时,p指向这个“够用”的块,q指向p后面(与链表方向相反)的一块(如上图p,q);

    往下,k很明确,计算空闲块中剩下的字节数;
    如果剩下的太小(<MIN_BLOCK),直接抛弃之,即将p指向的节点删除,即26行q->next = p->next;并返回空闲内存的地址&p[1](即绿色的开头处);
    继续往下(够大≥MIN_BLOCK),这四句结合起来才能看得懂:

       1: k-=HLEN;  // 空闲块内也要创建一个节点
       2: p->len=k;  // 此时的可用空间已经少size+sizeof(__mem__)
       3: q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k);  // 这里用的是切割的空闲块的后部
       4: q->len = size; // 这个新的节点仅用来记录分割了多少字节(便于free时回收),
       5: // 并没有链接为链表,next字段也就没有赋值

    最终情形是这样的:
    malloc_final

    其中,ret表示返回值,蓝色为调用malloc所返回的内存(称这段“白色+蓝色”的为Allocated block)。
    所以p->len(当前)变成了p->len(初识的)-size-sizeof(__mem__)。

    至此,malloc完成,切割后部的一大好处是,对于原来的链表,你只需要修改p->len即可;试想,如果切割前半部分,那么,空闲块内新创建的节点(上图蓝色左边)要插入到原来的空闲链表上,而且被切下的内存块前的节点(上图绿色左边)要从原来的空闲链表上删除,操作相对较麻烦。(嗯,你可以想象从一个挂满腊肉的肉架上切肉,“切下一块直接拿走”总是要比“把大块腊肉拿下,从穿孔的那头切下一块,再将剩下的那块穿上孔挂上架子”要来的简单。)

    小结

    malloc如此组织内存:用__mem_avail__[0]为链表头结点(因为malloc源码中只用了它的next字段,而没有用到它的len字段)的单链表(称其为free list)连接所有free block,而每个free block的结构如我上图所画,其中包含一个节点struct __mem__,之后是一段长度为len的可用内存。
    每次调用malloc(size)时从链表的第一个节点(__mem_avail__[0]-next)开始找,直到找到一个“足够大”(len字段比size大)的free block,在从其上切下一个(size+HLEN)的block(Allocated block)。
    如果len比size多出的字节数不多,就直接将这个节点从free list上移除;
    否则,将该free block切为两段,并将后一段交给malloc返回;实际切下的大小要比size多出一个链表节点的大小,而这多出的一个节点,仅用了len字段,用于记录当前malloc的长度,以便free之时准确将其回收到free list之上。

    3) FREE.C

    有了这一番分析,也能猜得出free是如何做到“内存回收”的。
    前面的类型定义完全一样,这里略去(应该定义到一个.h里,再各自inlcude)。

    直接上free的代码,free的注释较为准确:

       1: void free (
       2:   void _MALLOC_MEM_ *memp)
       3: {
       4: /*-----------------------------------------------
       5: FREE attempts to organize Q, P0, and P so that
       6: Q < P0 < P.  Then, P0 is inserted into the free
       7: list so that the list is maintained in address
       8: order.
       9: 
      10: FREE also attempts to consolidate small blocks
      11: into the largest block possible.  So, after
      12: allocating all memory and freeing all memory,
      13: you will have a single block that is the size
      14: of the memory pool.  The overhead for the merge
      15: is very minimal.
      16: -----------------------------------------------*/
      17: __memp__ q;        /* ptr to free block */
      18: __memp__ p;        /* q->next */
      19: __memp__ p0;        /* block to free */
      20:  
      21: /*-----------------------------------------------
      22: If the user tried to free NULL, get out now.
      23: Otherwise, get the address of the header of the
      24: memp block (P0).  Then, try to locate Q and P
      25: such that Q < P0 < P.
      26: -----------------------------------------------*/
      27: if ((memp == NULL) || (AVAIL.len == 0))
      28:   return;
      29:  
      30: p0 = memp;
      31: p0 = &p0 [-1];        /* get address of header */
      32:  
      33: /*-----------------------------------------------
      34: Initialize.
      35: Q = Location of first available block.
      36: -----------------------------------------------*/
      37: q = &AVAIL;
      38:  
      39: /*-----------------------------------------------
      40: B2. Advance P.
      41: Hop through the list until we find a free block
      42: that is located in memory AFTER the block we're
      43: trying to free.
      44: -----------------------------------------------*/
      45: while (1)
      46:   {
      47:   p = q->next;
      48:  
      49:   if ((p == NULL) || (p > memp))
      50:     break;
      51:  
      52:   q = p;
      53:   }
      54:  
      55: /*-----------------------------------------------
      56: B3. Check upper bound.
      57: If P0 and P are contiguous, merge block P into
      58: block P0.
      59: -----------------------------------------------*/
      60: if ((p != NULL) && ((((char _MALLOC_MEM_ *)memp) + p0->len) == p))
      61:   {
      62:   p0->len += p->len + HLEN;
      63:   p0->next = p->next;
      64:   }
      65: else
      66:   {
      67:   p0->next = p;
      68:   }
      69:  
      70: /*-----------------------------------------------
      71: B4. Check lower bound.
      72: If Q and P0 are contiguous, merge P0 into Q.
      73: -----------------------------------------------*/
      74: if ((((char _MALLOC_MEM_ *)q) + q->len + HLEN) == p0)
      75:   {
      76:   q->len += p0->len + HLEN;
      77:   q->next = p0->next;
      78:   }
      79: else
      80:   {
      81:   q->next = p0;
      82:   }
      83: }

    12~13行,求得当前malloc所得block的节点结构。
    17~25行的while(1)仍然是遍历链表,但退出条件已经不一样了,
    变成了:if ((p == NULL) || (p > memp)),退出时p指向的free block在memp之后,q在memp之前。
    如图:

    后面的两个if做检查,如果memp所在的block和p,q某一或两个相邻都将被合并为一个free block,否则只将他们所在的free block节点链接起来。如下,memp所在free block和q所指向的free block相邻的情形:
    free_adj
    其中蓝色(memp指向的)为要free的内存,p0所指block与p所指block相邻,所以会发生合并(修改前一个的len值),合并后情形如下:
    free_final
    两个block合并成功!

    4) INIT_MEM.C

    MALLOC.C和FREE.C中都没有看到数组__mem_avail__的真身(仅用extern做了声明,不会取得内存实体),原来它藏在了INTI_MEM.C里:

       1: __memt__ _MALLOC_MEM_ __mem_avail__ [2] =
       2:   { 
       3:     { NULL, 0 },    /* HEAD for the available block list */
       4:     { NULL, 0 }, /* UNUSED but necessary so free doesn't join HEAD or ROVER with the pool */
       5:   };

    INIT_MEM.C还定义了一个重要的函数:

    void init_mempool (
      void _MALLOC_MEM_ *pool, // address of the memory pool
      unsigned int size);             // size of the pool in bytes

    其源码如下:

       1: void init_mempool (
       2:   void _MALLOC_MEM_ *pool,
       3:   unsigned int size)
       4: {
       5:  
       6: /*-----------------------------------------------
       7: If the pool points to the beginning of a memory
       8: area (NULL), change it to point to 1 and decrease
       9: the pool size by 1 byte.
      10: -----------------------------------------------*/
      11:   if (pool == NULL)   {
      12:     pool = 1;
      13:     size--;
      14:   }
      15:  
      16: /*-----------------------------------------------
      17: Set the AVAIL header to point to the beginning
      18: of the pool and set the pool size.
      19: -----------------------------------------------*/
      20:   AVAIL.next = pool;
      21:   AVAIL.len  = size;
      22:  
      23: /*-----------------------------------------------
      24: Set the link of the block in the pool to NULL
      25: (since it's the only block) and initialize the
      26: size of its data area.
      27: -----------------------------------------------*/
      28:   (AVAIL.next)->next = NULL;
      29:   (AVAIL.next)->len  = size - HLEN;
      30:  
      31: }

    由这段代码印证了malloc源码中AVAIL为头结点的猜想,16~19行的注释可以看到,AVIL.len记录的是内存池的大小,而非一般节点的空闲内存的字节数。
    这里的过程是这样的:
    首先,将头结点指向内存(block)块的首地址pool,再将len修改为size(内存块的长度)。
    init_mem

    然后,在这个内存块(block)内部建立一个节点:
    init_mem_final

    5) REALLOC.C

    有了malloc和free想要实现realloc当然简单,realloc的源码如下:

       1: void _MALLOC_MEM_ *realloc (
       2:   void _MALLOC_MEM_ *oldp,
       3:   unsigned int size)
       4: {
       5: __memp__ p0;
       6: void _MALLOC_MEM_ *newp;
       7:  
       8: if ((oldp == NULL) || (AVAIL.len == 0))
       9:   return (NULL);
      10:  
      11: p0 = oldp;
      12: p0 = &p0 [-1];        /* get address of header */
      13:  
      14: if ((newp = malloc (size)) == NULL)
      15:   {
      16:   return (NULL);
      17:   }
      18:  
      19: if (size > p0->len)
      20:   size = p0->len;
      21:  
      22: memcpy (newp, oldp, size);
      23: free (oldp);
      24:  
      25: return (newp);
      26: }

    注:realloc可以理解为具有“延长”动态数组能力的一个函数,在你一次malloc的内存不够长时可以调用它;当然,你也可以直接调用它,但那么做事不安全的。

    因果

    可能你会有疑问:为什么在Keil中会有init_mempool?为什么Keil的malloc,free这么复杂(VC的malloc,free就很简单)?
    用过Keil的朋友都知道,Keil是用来开发嵌入式软件的,它编译出来的可执行文件不是windows的PE格式也不是Linux的ELF格式,而是HEX-80。
    有必要提一下VC中malloc的实现,VC中malloc调用了HeapAlloc,HeapAlloc是Windows API,实现从堆中申请内存的功能,除此之外,Windows API还提供了功能和realloc相似的HeapReAlloc,以及功能和相似的HeapFree。所以VC中malloc,free的实现要比Keil中简单。

    VC的编译的目标程序是在Windows上运行的,而windows系统本身已经提供了一套内存管理的功能(API就是使用这些功能的一种方式),所以其上的应用程序不需要写太多的内存管理的代码(Windows已经为你做好了)。VC编译出来的程序调用malloc,malloc调用HeapAlloc,而HeapAlloc的原型是:

       1: LPVOID WINAPI HeapAlloc(
       2:   _In_  HANDLE hHeap,
       3:   _In_  DWORD dwFlags,
       4:   _In_  SIZE_T dwBytes
       5: );

    传入的hHeap参数必须是一个可用的“堆”(通常用HeapCreate),就和init_mempool一样,HeapAlloc调用前也需要先调用HeapCreate,以及其他环境的初始化操作,只是这些都是运行库(Runtime Library)做的事。Windows程序运行在操作系统之上,操作系统和运行库会为你准备好一切;而这些我们是看不到的,所以看到这里的init_mempool可能会感到有点奇怪。

    而Keil是编译的程序往往是在裸机(没有操作系统)上运行的,所以你要想有“内存管理”的功能,就要你自己实现,而Keil的开发商早已想到了这点,所以他们帮你你实现了一个版本(即这里介绍的),你可以直接使用它。

    应用

    关于这个几个函数如何应用,Keil的帮助文档里给出了一个实例:

       1: #include <stdlib.h>
       2:  
       3: unsigned char xdata malloc_mempool [0x1000];
       4:  
       5: void tst_init_mempool (void) {
       6:   int i;
       7:   xdata void *p;
       8:  
       9:   init_mempool (&malloc_mempool, sizeof(malloc_mempool));
      10:  
      11:   p = malloc (100);
      12:  
      13:   for (i = 0; i < 100; i++)
      14:     ((char *) p)[i] = i;
      15:  
      16:   free (p);
      17: }

    开销

    Keil提供的此种方案可以让你像PC上的程序一样使用malloc和free;这种方式的一大好处是,你可以在此后重复使用一段内存。

    想要灵活自然就要付出代价。

    这里的代价主要在于Allocted block的“头部”,下面就来详细分析:
    在Keil中xdata*和unsigned int都是两个字节,所以一个节点的大小sizeof(__mem__) == 4
    每次malloc(size)的效率就(不考虑free block,即allocated block的利用率):
    size/(size+4)
    所以你应该尽量多申请一些内存,如果你只申请4个字节,利用率只有50%.
    (据之前malloc分析,其实可以再“抠门”一些,让malloc的头部只有两个字节,每次malloc就少“浪费”两个字节)

    除此之外,在malloc,free陆续调用多次之后,内存池在也不是当初的一大块了,它将被分为很多个小块,他们被串接在free list之上。此时再调用malloc就不是那么简单的事了,malloc从free list的头部开始查找,直到找到一个“够大的”free block这个过程是有时间开销的。单链表的查找是O(n)复杂度,但问题是这里的n不能由你直接决定。所以malloc的时间性能也就不那么稳定了。

    缺陷

    要使用malloc,必须先调用init_mempool为malloc,free创建一个“内存池”;通常可以把一个xdata数组的空间交由malloc和free管理。但我们常会纠结:我该给多少字节给Pool?我的MCU可能只有1024个字节可用,也可能更少。如果给多了,我就没有足够的空间存放其他数据了;如果给少了,可能很快malloc就不能从池中取得足够的内存,甚至耗尽整个Pool。而这里的init_mempool只能调用一次;因为如果发生第二次调用,唯一的一个free list的头部(AVIL)会被切断,此前的整个链表都将“失去控制”!

    总结

    尽管Keil这个方案存在着一些小的缺陷,但是总体来说还是不错的,可以说是——在有限的情况下做到了较好的灵活性。

    注:
    1.我所使用的Keil 版本:V4.24.00 for C51
    几个源码文件连接:
    INIT_MEM.C: http://www.oschina.net/action/code/download?code=23770&id=39701
    MALLOC.C: http://www.oschina.net/action/code/download?code=23770&id=39702
    FREE.C: http://www.oschina.net/action/code/download?code=23770&id=39703
    CALLOC.C:http://www.oschina.net/action/code/download?code=23770&id=39704
    REALLOC.C:http://www.oschina.net/action/code/download?code=23770&id=39705

  • 相关阅读:
    泛海精灵Alpha阶段回顾
    [Scrum]1.6
    【Scrum】1.5
    泛海精灵 Beta计划 (草案)
    【scrum】1.7
    学术搜索的Bug
    Linux下查看文件和文件夹大小
    求7的34次方
    去除给定的字符串中左边、右边、中间的所有空格的实现
    身份证18位验证
  • 原文地址:https://www.cnblogs.com/xusw/p/3259546.html
Copyright © 2011-2022 走看看