-
引言:说到动态申请(Dynamic Allocation)内存的好处,学过C/C++的人可能都有体会。运行时的灵活申请自然要比编码时的猜测好的多。而在内存受限情况下这种灵活性又有特别的好处——能让我们把有限的内存用的更充分。所以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()平时没怎么用过,最为好奇),看到了这样的代码:
这个函数很简单,它并没有直接获取内存,而是调用了malloc;看到这样的代码很容易想到——这是一个用来分配动态数组的函数。size是元素大小,len是数组长度。应该是这样用的:
// ... pBase = (int*)calloc(sizeof(int), 10); // 10个整数 // ...
在calloc里看的了 _MALLOC_MEM_ 让人不解,顺着CALLOC.C的#include找上去,看到了:
原来是这个… …(如果有同学不知道xdata是什么,可以简单的理解为“堆”。管它呢!)。
2) MALLOC.C
继续点开MALLOC.C(这份代码不短),一看到头部,猜到它可能是个链表:
很明显,这里的next用作链接;但是,len的作用暂时还不能确定(猜测:标识空闲块的长度,注释说的)。它们是这样:
接下来是类型、常量定义定义:
typedef struct __mem__ __memt__; typedef __memt__ _MALLOC_MEM_ *__memp__; #define HLEN (sizeof(__memt__)) extern __memt__ _MALLOC_MEM_ __mem_avail__ []; #define AVAIL (__mem_avail__[0]) #define MIN_BLOCK (HLEN * 4)
看到这些typedef,#define也不能确定各自是做什么用的。但是有个extern声明的数组!应该在别的地方有定义。(关于声明和定义不多说了)
然后就是完整的malloc()了(部分注释已被删除):
void _MALLOC_MEM_ *malloc( unsigned int size) { __memp__ q; /* ptr to free block */ __memp__ p; /* q->next */ unsigned int k; /* space remaining in the allocated block */ q = &AVAIL; while (1) { if ((p = q->next) == NULL) { return (NULL); /* FAILURE */ } if (p->len >= size) break; q = p; } k = p->len - size; /* calc. remaining bytes in block */ if (k < MIN_BLOCK) /* rem. bytes too small for new block */ { q->next = p->next; return (&p[1]); /* SUCCESS */ } k -= HLEN; p->len = k; q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k); q->len = size; return (&q[1]); /* SUCCESS */ }
稍加分析可知,while(1)是循环遍历链表的(循环内的p=q->next和q=p这两句)。所以q刚开始指向的应该是链表的头结点,AVAIL即__mem_avail__[0]里存放着链表的头结点。
由16行 if(p->len > size) break; 可知,len的作用确实是用来标识空闲块的长度;
所以整个链表应该是这样的(绿色部分为空闲内存,白色是链表节点):
由此可知,注释里的free block指的是一个“白色+绿色”。注意,一旦满足条件(找到一个足够大的空闲块),跳出循环时,p指向这个“够用”的块,q指向p的前驱(与链表方向相反的一块)(如上图p,q);
往下,k很明确,计算空闲块中剩下的字节数;
如果剩下的太小(<MIN_BLOCK),直接抛弃之,即将p指向的节点删除,即26行q->next = p->next;并返回空闲内存的地址&p[1](即绿色的开头处);
继续往下(够大≥MIN_BLOCK),这四句结合起来才能看得懂:k-=HLEN; // 空闲块内也要创建一个节点 p->len=k; // 此时的可用空间已经少size+sizeof(__mem__) q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k); // !切下的是空闲块的后部 q->len = size; // 这个新的节点仅用来记录分割了多少字节(便于free时回收), // 并没有链接为链表,next字段也就没有赋值
最终情形是这样的:
其中,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。如果len比size多出的字节数不多,就直接将这个节点从free list上移除,并直接返回当前的可用内存地址(绿色的开头);
否则,将该free block切为两段,并将后一段交给malloc返回;实际切下的大小要比size多出一个链表节点的大小,而这多出的一个节点,仅用了len字段,用于记录当前malloc的长度,以便free之时准确将其回收到free list之上。(注:这里有点浪费)3) FREE.C
有了这一番分析,也能猜得出free是如何做到“内存回收”的。
前面的类型定义完全一样,这里略去(应该定义到一个.h里,再各自inlcude)。直接上free的代码,free的注释较为准确:
void free ( void _MALLOC_MEM_ *memp) { /*----------------------------------------------- FREE attempts to organize Q, P0, and P so that Q < P0 < P. Then, P0 is inserted into the free list so that the list is maintained in address order. FREE also attempts to consolidate small blocks into the largest block possible. So, after allocating all memory and freeing all memory, you will have a single block that is the size of the memory pool. The overhead for the merge is very minimal. -----------------------------------------------*/ __memp__ q; /* ptr to free block */ __memp__ p; /* q->next */ __memp__ p0; /* block to free */ /*----------------------------------------------- If the user tried to free NULL, get out now. Otherwise, get the address of the header of the memp block (P0). Then, try to locate Q and P such that Q < P0 < P. -----------------------------------------------*/ if ((memp == NULL) || (AVAIL.len == 0)) return; p0 = memp; p0 = &p0 [-1]; /* get address of header */ /*----------------------------------------------- Initialize. Q = Location of first available block. -----------------------------------------------*/ q = &AVAIL; /*----------------------------------------------- B2. Advance P. Hop through the list until we find a free block that is located in memory AFTER the block we're trying to free. -----------------------------------------------*/ while (1) { p = q->next; if ((p == NULL) || (p > memp)) break; q = p; } /*----------------------------------------------- B3. Check upper bound. If P0 and P are contiguous, merge block P into block P0. -----------------------------------------------*/ if ((p != NULL) && ((((char _MALLOC_MEM_ *)memp) + p0->len) == p)) { p0->len += p->len + HLEN; p0->next = p->next; } else { p0->next = p; } /*----------------------------------------------- B4. Check lower bound. If Q and P0 are contiguous, merge P0 into Q. -----------------------------------------------*/ if ((((char _MALLOC_MEM_ *)q) + q->len + HLEN) == p0) { q->len += p0->len + HLEN; q->next = p0->next; } else { q->next = p0; } }
30~31行,求得当前malloc所得block的节点结构。
45~53行的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相邻的情形:
其中蓝色(memp指向的)为要free的内存,p0所指block与p所指block相邻,所以会发生合并(修改前一个的len值),合并后情形如下:
两个block合并成功!4) INIT_MEM.C
MALLOC.C和FREE.C中都没有看到数组__mem_avail__的真身(仅用extern做了声明,不会取得内存实体),原来它藏在了INTI_MEM.C里:
__memt__ _MALLOC_MEM_ __mem_avail__ [2] = { { NULL, 0 }, /* HEAD for the available block list */ { NULL, 0 }, /* UNUSED but necessary so free doesn't join HEAD or ROVER with the pool */ };
INIT_MEM.C还定义了一个重要的函数:
void init_mempool ( void _MALLOC_MEM_ *pool, // address of the memory pool unsigned int size); // size of the pool in bytes
其源码如下:
void init_mempool ( void _MALLOC_MEM_ *pool, unsigned int size) { /*----------------------------------------------- If the pool points to the beginning of a memory area (NULL), change it to point to 1 and decrease the pool size by 1 byte. -----------------------------------------------*/ if (pool == NULL) { pool = 1; size--; } /*----------------------------------------------- Set the AVAIL header to point to the beginning of the pool and set the pool size. -----------------------------------------------*/ AVAIL.next = pool; AVAIL.len = size; /*----------------------------------------------- Set the link of the block in the pool to NULL (since it's the only block) and initialize the size of its data area. -----------------------------------------------*/ (AVAIL.next)->next = NULL; (AVAIL.next)->len = size - HLEN; }
由这段代码印证了malloc源码中AVAIL为头结点的猜想,16~19行的注释可以看到,AVIL.len记录的是内存池的大小,而非一般节点的空闲内存的字节数。
这里的过程是这样的:
首先,将头结点指向内存(block)块的首地址pool,再将len修改为size(内存块的长度)。
然后,在这个内存块(block)内部建立一个节点:
5) REALLOC.C
有了malloc和free想要实现realloc当然简单,realloc的源码如下:
void _MALLOC_MEM_ *realloc ( void _MALLOC_MEM_ *oldp, unsigned int size) { __memp__ p0; void _MALLOC_MEM_ *newp; if ((oldp == NULL) || (AVAIL.len == 0)) return (NULL); p0 = oldp; p0 = &p0 [-1]; /* get address of header */ if ((newp = malloc (size)) == NULL) { return (NULL); } if (size > p0->len) size = p0->len; memcpy (newp, oldp, size); free (oldp); return (newp); }
注: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的原型是:
LPVOID WINAPI HeapAlloc( _In_ HANDLE hHeap, _In_ DWORD dwFlags, _In_ SIZE_T dwBytes );
传入的hHeap参数必须是一个可用的“堆”(通常用HeapCreate),就和init_mempool一样,HeapAlloc调用前也需要先调用HeapCreate,以及其他环境的初始化操作,只是这些都是运行库(Runtime Library)做的事。Windows程序运行在操作系统之上,操作系统和运行库会为你准备好一切;而这些我们是看不到的,所以看到这里的init_mempool可能会感到有点奇怪。
而Keil是编译的程序往往是在裸机(没有操作系统)上运行的,所以你要想有“内存管理”的功能,就要你自己实现,而Keil的开发商早已想到了这点,所以他们帮你你实现了一个版本(即这里介绍的),你可以直接使用它。
应用
关于这个几个函数如何应用,Keil的帮助文档里给出了一个实例:
#include <stdlib.h> unsigned char xdata malloc_mempool [0x1000]; void tst_init_mempool (void) { int i; xdata void *p; init_mempool (&malloc_mempool, sizeof(malloc_mempool)); p = malloc (100); for (i = 0; i < 100; i++) ((char *) p)[i] = i; free (p); }
开销
Keil提供的此种方案可以让你像标准C程序一样使用malloc和free;这种方式的一大好处是,你可以在此后重复使用一段内存。
想要灵活自然就要付出代价。
空间代价主要在于Allocted block的“头部”,下面就来详细分析:
在Keil中xdata*和unsigned int都是两个字节,所以一个节点的大小sizeof(__mem__) == 4
每次malloc(size)的效率就(不考虑free block,即allocated block的利用率):
size/(size+4)
所以你应该尽量多申请一些内存,如果你只申请4个字节,利用率只有50%.
(据之前malloc分析,其实可以再“抠门”一些,让malloc所得block的头部只记录长度(因为next字段没有使用),每次malloc就少“浪费”两个字节)时间上,在malloc,free陆续调用多次之后,内存池在也不是当初的一大块了,它将被分为很多个小块,他们被串接在free list之上。
此时调用malloc就不是那么简单的事了,malloc从free list的头部开始查找,直到找到一个“够大的”free block这个过程是有时间开销的。单链表的查找是O(n)复杂度,但问题是这里的n不能由你直接决定。所以malloc的时间性能也就不那么稳定了。
调用free也是同样,在free list上挂的节点变多时,每次free都要从头开始找,找到能做block的前驱的block(被free源码中的q所指)之后,再将当前的block插入到其后。完成该操作必须修改q所指节点,而你没有指向该block指针,必然要从头查找。所以free通常情况下的时间复杂度是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