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

时间:2021-06-23 19:38:49

最近翻看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的作用暂时还不能确定(猜测:标识空闲块的长度,注释说的)。它们是这样:

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

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

   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的作用确实是用来标识空闲块的长度;
所以整个链表应该是这样的(绿色部分为空闲内存,白色是链表节点):
Pooled Allocation池式分配实例——Keil 内存管理
由此可知,注释里的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字段也就没有赋值

最终情形是这样的:
Pooled Allocation池式分配实例——Keil 内存管理

其中,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相邻的情形:
Pooled Allocation池式分配实例——Keil 内存管理
其中蓝色(memp指向的)为要free的内存,p0所指block与p所指block相邻,所以会发生合并(修改前一个的len值),合并后情形如下:
Pooled Allocation池式分配实例——Keil 内存管理
两个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(内存块的长度)。
Pooled Allocation池式分配实例——Keil 内存管理

然后,在这个内存块(block)内部建立一个节点:
Pooled Allocation池式分配实例——Keil 内存管理

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