Pooled Allocation池式分配实例——Keil 内存管理

最近翻看Kei安装目录,无意中发现C51\LIB下的几个.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

posted @ 2013-08-15 13:47  码工许师傅  阅读(1616)  评论(1编辑  收藏  举报