前言
老生常谈的问题。单片机那么点RAM用得着内存管理吗?我可以回答,99%的情况都用不到,但是有些技术就是为了那1%存在的。
实现内存管理的原理
一个100间房的宾馆年接客30000人。实现的原理就是内存管理的原理。3万个客人不会同时订房,同样应用程序一般也不会同时使用内存。客人退房后打扫干净住下一个客人,同样一个程序使用完一段内存,回收再分配给另一个程序。
宾馆要给客人开房,就要知道那间房空着,同要分配内存就要知道哪个内存还没被使用。开好房后要把钥匙给客人,同理分配好内存要把内存的地址告诉应用程序。
基于状态表的内存管理
要实现内存管理,最核心的问题就是要知道哪些内存空着,哪些已经被分配。
我们可以把可分配的连续内存均匀划分成很多份,每一份叫做内存块。然后再建一个表,表里的每个元素一一对应内存块,标记没个内存块是否被分配。比如某个内存块被分配了,就往表里对应的元素写1,释放了就写回0表示内存空闲
单单只是标记了分配还是未分配还是有问题,因为应用程序不一定就只用到1个内存快的内存。如上图APP1data用到2个内存块,但是从内存表上只能看出前3个连续的内存块被分配,但是没法看出是一起分配给了同一个应用,还是分配给不同的应用。那么要释放内存时,我们就不知道要释放几个内存块。那么内存状态表的元素需要记录更多的信息才行。
内存分配出去后,应用程序唯一会保留的信息就是分配的内存首地址。释放内存也是靠这个地址。要释放肯定就要操作状态表,首先 (分配出去的地址 - 内存池首地址)÷ 内存块大小 = 分配出去的内存是第几个内存块开始。 我们也就知道了状态标志是第几个开始。定位了状态标志,那么就是要取得为程序分配了的内存大小或内存块数量,这个就是要记录的第二个信息。
那么状态表的标志改为0是空,非0是已分配,并且这个非0的数就是该程序分配的内存快大小。如下图,由APPdata1的首地址可以定位到状态表元素是第1个元素(0开始计数),第1个元素的值是2代表APPdata1分配了2个内存块。同理APPdata2也是2个内存块。至于状态表的第2个和第4个元素也是2,只是因为要写非0值区分空闲内存,因此也直接写等于分配了的内存块数,其实写任何非0值都可以。
程序实现
#define MEM_BLOCK_SIZE 128//每个内存块字节
#define MEM_MAX_SIZE 32*1024 //内存池总大小 32K字节 要是内存块的整数倍
//内存池(32字节对齐)
__align(32) u8 membase[MEM_MAX_SIZE]; //内存池数组 4字节对齐
u16 memmapbase[MEM_MAX_SIZE/MEM_BLOCK_SIZE]; //内存状态表
//设置内存
//*s:内存首地址
//c :要设置的值
//count:需要设置的内存大小(字节为单位)
void mymemset(void *s,u8 c,u32 count)
{
u8 *xs = s;
while(count--)*xs++=c;
}
//内存管理初始化
//memx:所属内存块
void my_mem_init(u8 memx)
{
mymemset(membase, 0,MEM_MAX_SIZE );//内存状态表数据清零
}
//memx:所属内存块
//size:要分配的内存大小(字节)
//返回值:0XFFFFFFFF,代表错误;其他,内存偏移地址
u32 my_mem_malloc(u32 size)
{
signed long offset=0;
u32 nmemb; //需要的内存块数
u32 cmemb=0;//连续空内存块数
u32 i;
if(size==0)return 0XFFFFFFFF;//不需要分配
nmemb=size/MEM_BLOCK_SIZE //获取需要分配的连续内存块数
if(size%MEM_BLOCK_SIZE)nmemb++;
for(offset=MEM_MAX_SIZE/MEM_BLOCK_SIZE;offset>=0;offset--)//搜索整个内存控制区
{
if(memmapbase[offset]==0)cmemb++;//连续空内存块数增加
else cmemb=0; //连续内存块清零
if(cmemb==nmemb) //找到了连续nmemb个空内存块
{
for(i=0;i<nmemb;i++) //标注内存块非空
{
memmapbase[offset+i]=nmemb;
}
return (offset*MEM_BLOCK_SIZE);//返回偏移地址
}
}
return 0XFFFFFFFF;//未找到符合分配条件的内存块
}
//释放内存(内部调用)
//memx:所属内存块
//offset:内存地址偏移
//返回值:0,释放成功;1,释放失败;
u8 my_mem_free(u8 memx,u32 offset)
{
int i;
if(offset<MEM_MAX_SIZE)//偏移在内存池内.
{
int index=offset/MEM_BLOCK_SIZE; //偏移所在内存块号码
int nmemb=memmapbase[index]; //内存块数量
for(i=0;i<nmemb;i++) //内存块清零
{
memmapbase[index+i]=0;
}
return 0;
}else return 2;//偏移超区了.
}
//释放内存(外部调用)
//memx:所属内存块
//ptr:内存首地址
void myfree(u8 memx,void *ptr)
{
u32 offset;
if(ptr==NULL)return;//地址为0.
offset=(u32)ptr-(u32)membase;
my_mem_free(memx,offset); //释放内存
}
//分配内存(外部调用)
//memx:所属内存块
//size:内存大小(字节)
//返回值:分配到的内存首地址.
void *mymalloc(u8 memx,u32 size)
{
u32 offset;
offset=my_mem_malloc(memx,size);
if(offset==0XFFFFFFFF)return NULL;
else return (void*)((u32)membase+offset);
}
基于链表的形式
上边用状态表的方式是额外用数组来记录状态,而且内存只能按块分配。下面介绍可以分配任意大小的方式。
基本思想
用状态表的方式我能能通过遍历数组知道那些内存空着,而另外一种能实现遍历功能的就是链表。首先定义一个链表结构体保存信息:
typedef struct node* PNode;//定义节点指针
typedef struct node
{ u32 offset; //与内存池首地址的偏移 (当前节点位置)
u32 size;//本节点的内存大小
PNode Prior;//前驱指针域 (下一节点位置)
PNode Pnext;//后继指针域 (前一节点位置)
}Node;
如上图,每次分配内存就是创建一个节点,而且节点存放的位置就是分配的内存前边。
这样我们顺着链表节点遍历下去,能知道这段内存的位置,大小。还可以计算节点间空闲内存大小 如(节点4的位置 - 节点3的位置)- 节点3的内存大小 = 节点3和节点4之间空闲内存的大小。要分配这些内存。只要在节点3和4间再插入一个节点就行。要释放内存更简单,应为分配出去的内存地址的前边就是节点信息,能直接点位节点。直接在链表里删除该节点就行。
程序实现
#define MEM_MAX_SIZE (32*1024) //最大管理内存 32K
typedef struct node* PNode;//定义节点指针
typedef struct node
{ u32 offset; //与内存首地址的偏移
u32 size;//本节点带的内存大小
PNode Prior;//前驱指针域
PNode Pnext;//后继指针域
}Node;
__align(4) u8 membase[MEM_MAX_SIZE]; //SRAM内存池 4字节对齐
Node HedaNode,EndNode; //头尾节点
void mem_init() //初始化内存链表
{
HedaNode.offset=0; //头节点初始化
HedaNode.size=0;
HedaNode.Prior=NULL;
HedaNode.Pnext=&EndNode; //刚开始指向尾节点
EndNode.offset=MEM_MAX_SIZE;//尾节点初始化
EndNode.size=0;
EndNode.Prior=&HedaNode; //刚开始指向头节点
EndNode.Pnext=NULL;
}
//设置内存
//*s:内存首地址
//c :要设置的值
//count:需要设置的内存大小(字节为单位)
void mymemset(void *s,u8 c,u32 count)
{
u8 *xs = s;
while(count--)*xs++=c;
}
//内存分配
//size:需要分配的内存大小(字节为单位)
void *mymalloc(u32 size)
{
PNode p=NULL;
PNode pnew=NULL;
u32 relsize;
u32 menaddr;
relsize=size+sizeof(Node); //实际要申请的内存大小 包含节点空间
if(relsize%4)//4字节对齐
relsize+=(4-(relsize%4)); //不满4字节就补齐
p=&HedaNode; //导入头节点
while(p->Pnext)//下一节点存在
{
if((p->Pnext->offset - (p->offset + p->size))>relsize)//本节点和下节点间剩余的内存>要申请的内存大小
{ menaddr=(u32)membase+(p->offset)+(p->size);
pnew=(PNode)menaddr;//新节点的地址=内存池基地址+本节点偏移量+本节点内存大小
pnew->Prior=p;
pnew->Pnext=p->Pnext; //新节点的Pnext=本节点Pnext
pnew->size=relsize; //新节点的内存大小
pnew->offset=p->offset+p->size; //新节点偏移量
p->Pnext=pnew; //本节点Pnext=新节点的地址
mymemset( pnew+1,0,size); //初始化内存
return (void*)(( (PNode)menaddr )+1); //返回去除节点信息的地址
}
else
p=p->Pnext; //导入下一个节点
}
return NULL; //申请失败
}
//释放内存
//ptr:内存首地址
void myfree(void *ptr)
{
PNode p=NULL;
if(ptr==NULL)return;//地址为0.
p=((PNode)ptr)-1;//点位到内存地址前的节点信息
p->Prior->Pnext=p->Pnext;//本上节点的上一节点的Pnext=本节点的下一节点地址
p->Pnext->Prior=p->Prior;//本上节点的下一节点的Prior=本节点的上一节点地址
p->Prior=NULL;
p->Pnext=NULL;
}
链表方式能实现分配任意大小内存,而且不用额外的状态表。但是一旦应用程序操作内存越界误操作了节点信息,整个内存链表就会断裂。