【@.1 指针与动态内存管理】
在C语言中的指针若不经过初始化是无法进行操作的,在编译时甚至不会报错,但是一旦运行到这里时就会出现程序错误死机。所以对于指针的操作一定要首先初始化再赋值。考虑如下代码:
void foo1(void) { unsigned char * pdata; ... *pdata = 0x0f; }
当运行到pdata赋值时由于没有初始化,程序必死。当然这还是比较明显的错误,若指针被封装在结构或联合中就不用已发现了,比如如下代码:
#include <stdio.h> typedef struct{ void * next; unsigned int data; }NodeType; NodeType *pNodes, Node1, Node2; int main(void) { Node1.next = &Node2; Node1.data = 0xff; pNodes->next=(void*)0; //Wrong! printf("hello!"); return 1; }
上面程序可能不会报错,而且打印的东西跟pNodes没关系,但实际运行程序会死掉,关键就是那句pNodes->node的赋值句,这里看上去是第一次给pNodes赋值,以为是初始化,但是实际上操作了一个没有初始化的内存地址程序会死机。好的方式是想下面这样写主函数(其余部分一样):
int main(void) { Node1.next = &Node2; Node1.data = 0xff; pNodes = &Node1; printf("%d",pNodes->data); return 1; }
这里直接将Node1的地址复制给pNodes,此时pNodes才能被正常初始化。所以一般我们在程序中会采取动态内存分配的方式申请内存空间,为指针型变量分配内存,比如如下代码:
int main(void) { pNodes = (NodeType*)malloc(sizeof(NodeType)); pNodes->next = &Node1; printf("%d",pNodes->data); return 1; }
调用malloc函数之后就可以随便操作这个指针变量了。参考这里可以看malloc的具体使用方法。但是这种malloc函数是一种平台相关函数,不同的CPU,不同的操作系统的具体实现内容不一样。另外对于嵌入式系统来说,写代码时调用这类函数并不妥当,可能会使本来就吃紧的内存被分割成许多小而不连续的内存块。因此若编写嵌入式代码,甚至是编写PC代码时想自己手动了解变量分配的机制并加以内存管理时,可以考虑自己编写这类内存管理分配的代码。以下的代码就实现了这一功能,编程思想跟uCOS-II中的一模一样,你可以看做是一个uCOS-II中内存管理的分析,但是这种机制完全可以在PC机上使用,而实际上我的代码测试也是在window下完成的。如果你手上没有一个纯洁的C语言编译器,可以参考我的这篇博客利用gcc来进行C语言编译。
【@.2 uCOS-II中动态内存管理的C语言实现】
嵌入式编程比较关心的就是内存大小,在有限的内存中实现动态和静态的代码分配是有学问的。对于像malloc()这类在运行时从堆(heap)中请求内存的函数,若调用次数太多可能会造成内存的快速消耗殆尽。一个比较好的放大是,放弃使用malloc系列函数,将所有将要运行时分配的空间计算好,利用全局变量分配到静态代码空间中,实际使用时调用自己编写好的内存管理函数从这块静态空间中申请内存。这样的一个最大的好处是,内存的总体大小是受控的,所有涉及到动态分配内存的地方实际上都已经被预先分配好,在编译时写在静态代码区(利用全局变量)。若对于程序静态和动态的内存需求还不清楚的可以参考我的这一篇博客有一个比较简单但是通俗易懂的介绍。因此,第一步需要做的事就是申请一个全局变量存放将要动态分配的内存,比如下面的全局变量申明:
INT8U Buff8U [40][32]; //Store runtime data
INT16U Buff16U [40][32]; //Store runtime data
这里采用了二维数组。这篇博客利用一维数组构建了内存管理机制,可以参考。对于二维数组,左边的可以看做分区,右边的下标可以看做是在不同分区中的内存块,但是整个数组在内存空间中还是线性分布的,可参考如下图示:
每个这样的二维数组就可以看成是一个连续的内存空间。如果程序要求层次结构不高,在程序中甚至可以仅仅包含这一个二维数组,只要计算好大小即可。
有了这样的二维数组之后,还需要另一个内存管理块来对这片内存进行管理,如下定义:
typedef struct{ void *EntryAddr; //Point to the addr of storage array void *FreeAddr; //Point to current free array INT32U BlkNum; //Number of blocks INT32U BlkSize; //Size of each block INT32U BlkFreeNum; //Free block number can be used }MMUType; MMUType *MMUBuff8; //Controller of Buff8U MMUType *MMUBuff16; //Controller of Buff16U
前面代码中的两个控制块这里只画了一个,每个内存管理块中记录了对应存储的数组地址(EntryAddr),当前可用的数组的地址(FreeAddr),当内存进行分配时,这一位会进行偏移操作,指向还未被分配的存储数组。而这些管理块也需要一个总的数组来存储其位置(MMUPool[ ]),否则这些指针也是无法初始化的指针。并且新建一个指针(*MMUFree)指向当前还未被分配的,可以用的管理块数组中位置。
MMUType MMUPool[MMU_BUFF_MAX]; //Store all the MMUs MMUType *MMUFree; //Pointer to the available MMUType in MMUPool MMUType *MMUBuff8; //Controller of Buff8U MMUType *MMUBuff16; //Controller of Buff16U
图中红色的就是实际存储的数组。可以看出核心思想是,所有需要实际存储的指针都由数组来保存,所有指针最终均指向一个实际的变量。若要使用这种内存管理,第一步要做的是初始化MMUPool,调用MemInit()之后将内部所有的成员形成一个链表,由MMUFree指向最开头的一个。
这样一来就可以为内存管理块分配一个实际的对象了。调用MemCreate()可以分配一个实际的对象给内存管理块并且将内存管理块与实际的内存存储区联系在一起
这一步之后就可以分配实际的内存给指针了,比如如下代码:
void foo(void){
unsigned short *data;
unsigned char err;
data=(unsigned short*)Memloc(MMUBuff16,&err);
//…
}
当最后不用这篇内存时,一定要记得调用MemFree()回收这篇内存空间,不然这片内存空间将会越来越小导致无法再次使用这篇内存空间。所以这也是C语言没有垃圾回收机制的一个弊端,虽然我们很多时候并没有注意这件事,但是一旦程序编的很长,这些内存的细节就不得不注意了。
void foo(void){
unsigned short *data;
unsigned char err;
data=(unsigned short*)Memloc(MMUBuff16,&err);
//…
MemFree(MMUBuff16,data); //一定记得要回收内存空间到原控制块
}
这种手动内存回收没有重新清零,所以当下次再分配内存时将会把这篇包含数据的使用过的内存分配给新的对象,有可能造成内存泄露。而实际上在uCOS-II源代码中,并没有进行重新清零的操作,当然像工业控制中这类问题不是很重要,所以可以忽略。而若要编写实际的操作系统,这种简单的回收机制就不适用了。
整个完整的流程可以用下面的C代码写出,这里的实现思想基本跟uCOS-II中的一样,但是可以在PC上进行编译,这样测试的效果也很明显。注意有的C++编译器可能会报错(比如Visual Studio),可以参考我的这篇博客使用gcc的C编译器来测试。
/******************************************************************************** * MMU.c * @author. apollius * @date. 03/25/2013 Mon * @brief. A tiny Memory Management controller in C, almost the same as that in * uCOS-II, demonstrate some basic ideas in memory management. * Successed in gcc toolchain ********************************************************************************/ #include <stdio.h> #define INT8U unsigned char #define INT16U unsigned short #define INT32U unsigned int #define MMU_BUFF_MAX 6 //The largest number to store MMUType typedef struct{ void *EntryAddr; //Point to the addr of storage array void *FreeAddr; //Point to current free array INT32U BlkNum; //Number of blocks INT32U BlkSize; //Size of each block INT32U BlkFreeNum; //Free block number can be used }MMUType; MMUType MMUPool[MMU_BUFF_MAX]; //Store all the MMUs MMUType *MMUFree; //Pointer to the available MMUType in MMUPool MMUType *MMUBuff8; //Controller of Buff8U MMUType *MMUBuff16; //Controller of Buff16U INT8U Buff8U [40][32]; //Store runtime data INT16U Buff16U [40][32]; //Store runtime data static void MemInit(void); static MMUType *MemCreate(void *buffaddr, INT32U blkn, INT32U blksize, INT8U * err); static void *Memloc(MMUType *pmem, INT8U *err); static INT8U MemFree(MMUType *pmem, void * pblk); int main(void) { INT8U err; INT16U *data; INT32U i; MemInit(); //Must be called first to use the runtime memory allocation err=0; MMUBuff16=MemCreate(Buff16U,60,32,&err); //Link MMUBuff16 with Buff16U data=(INT16U*)Memloc(MMUBuff16,&err); //malloc pointer *data for(i=0; i<32; i++) data[i]=i+2; for(i=0; i<32; i++) printf("%#04x ",data[i]); MemFree(MMUBuff16,data); //If not use, you must free it manually //Do not free data to other MMUcontroler like MMUBuff8, since it is malloced //from MMUBuff16. It may not cause error, but it's not a good habit printf("\r\n"); for(i=0; i<32; i++) printf("%#04x ",*((INT8U*)MMUBuff16->FreeAddr+i)); //Here can see while the MMUBuff16 is freed, it still contains datas -> memory leak return 1; } static void MemInit(void){ MMUType *pmmu; INT32U i; for(i=0;i<MMU_BUFF_MAX-1;i++){ MMUPool[i].EntryAddr = (void*)0; pmmu = &MMUPool[i+1]; MMUPool[i].FreeAddr = (void*)pmmu; //Point .FreeAddr to next MMUType*, it's different from the behavior in MemCreate() } MMUPool[MMU_BUFF_MAX].EntryAddr = (void*)0; MMUPool[MMU_BUFF_MAX].FreeAddr = (void*)0; MMUFree = &MMUPool[0]; //MMUFree point to the first pool } static MMUType *MemCreate(void * buffaddr, INT32U blkn, INT32U blksize, INT8U * err){ MMUType *pmem; void **plink; void *pblk; INT32U i; //If MMUPool is full, return void* 0, set *err=1 if(MMUFree->FreeAddr ==(void*)0){ *err = 1; return (MMUType*)0; } pmem = (MMUType*)MMUFree->FreeAddr; MMUFree = (MMUType*)MMUFree->FreeAddr; plink = (void**) buffaddr; pblk = (void* ) buffaddr + blksize; for(i=0; i<blkn-1; i++){ *plink = pblk; plink = (void**)pblk; pblk = pblk + blksize; } *plink = (void*)0; //Last node point to void* 0 pmem->EntryAddr = buffaddr; pmem->FreeAddr = buffaddr; pmem->BlkNum = blkn; pmem->BlkSize = blksize; pmem->BlkFreeNum = blkn; *err = 0; return pmem; } static void *Memloc(MMUType *pmem, INT8U *err){ void *pblk; if(pmem->BlkFreeNum>0){ pblk = pmem->FreeAddr; pmem->FreeAddr = *(void**)pblk; pmem->BlkFreeNum--; *err = 0; return pblk; } else{ *err=1; return (void*)0; } } static INT8U MemFree(MMUType *pmem, void * pblk){ if(pmem->BlkFreeNum >= pmem->BlkNum){ return 1; //error } *(void**)pblk = pmem->FreeAddr; pmem->FreeAddr = pblk; pmem->BlkFreeNum++; /*Note there is no re-clear memory function*/ return 0; }
@.[FIN] @.date->Mar 26, 2012 @.author->apollius