分析UCOS中的内存管理
一、前言:
在嵌入式设备中,经常会存在一些任务需要大量的内存,在内存相对较少的MCU中,有效的管理宝贵的内存资源,是一个重要的问题。
在PC上位机编程中调用malloc()和free()来申请和释放内存,对内存管理。相对于嵌入式系统而言,上位机内存很大,而且Windows提供了很好的内存管理接口,所以使用这些函数没有问题。但在嵌入式中持续的调用malloc()和free()会出现两个问题:
1、产生内存碎片。
运行过程中,各个任务频繁的调用内存申请和释放,会导致原本一整块空间地址连续的区域分散成一堆物理地址上相对独立的区域,这会导致一个任务需要较大的内存,空余的内存块没有一个连续的地址,无法分配给任务。长时间运行,到最后系统可能连一个很小的物理地址都分配不到,导致系统的崩溃。
2、运行时间不确定。
在free()函数中,存在着一些内存合并等功能,例如将释放完成后,将空间上相近的两个空白区域合并为同一个将存在内存的区域重新整合,甚至可能使用了二叉树等非线性数据结构,等等操作。
而这些函数所耗费的时间是无法确定的,在实际的应用中,对于内存这种全局变量,多个任务都要用到,为避免会存在这种重入性的问题,必须采用信号同步的方法,或者暂时关闭中断的方法,来同步对各个任务对共享资源的使用。这样,导致了系统死区时间的增加,相应速度的变慢,不确定性增加。
在大多数嵌入式系统中,通常采用静态内存池的方法。将系统空余的内存同一管理,生成一系列的大小固定的内存块池。在实际的操作中,以这一整个内存块进行操作。
分析下UCOS内存管理机制,在系统初始化的时候就分配好内存空间,将所有可能的空间组织成链表,需要申请内存的时候直接从链表中申请,释放内存的时候直接将内存归还空余内存链表中即可。使用这种方法避免内存碎片的产生,而且使得在常数时间内分配内存空间称为可能。
二、内存控制块
MCB用来定义一个内存区域的属性。这种类似面向对象的控制结构在ucos里很常见,只要在TCB、ECB和MCB这样的控制块中,加入一些方法,这个控制块瞬间变成一个类。
MCB原型:
typedef struct { void *OSMemAddr; void *OSMemFreeList; INT32U OSMemBlkSize; INT32U OSMemNBlks; INT32U OSMemNFree; } OS_MEM; |
OSMemAddr 是指向内存分区起始地址的指针。它在建立内存分区时被初始化,在此之后就不能更改了。
OSMemFreeList 是指向下一个空闲内存控制块或者下一个空闲的内存块指针。
OSMemBlkSize和OSMemNBlks是内存分区中内存的总大小和内存块数量,是用户建立该内存分区时指定的。
OSMemNFree 是内存分区中当前可以得空闲内存块数量。
三、相关函数
3.1、创建一个内存分区,OSMemCreate();
在建立一个内存分区前,用户需要自己先在全局定义好这个分区和一个内存控制块指针。
OS_MEM *CommTxBuf; INT8U CommTxPart[100][32]; |
然后将定义好的区域作为参数传给OSMemCreate(),使用指针接收函数的返回值。
CommTxBuf = OSMemCreate(CommTxPart, 100, 32, &err); |
这个函数最主要的功能,就是初始化一个Mcb结构。将起始地址、总大小、分块数量、初始空闲内存等等写入一个Mcb,然后这个Mcb的指针作为函数的返回值。之后对该块内存的申请、释放操作都将通过这个指针来进行,和Ecb完全一样。
这里值得一提的是空闲内存块表,它是一个指针单向链表,Freelist指向它的第一个元素。初始化代码如下:
plink = (void **)addr; pblk = (INT8U *)addr + blksize; for (i = 0; i < (nblks - 1); i++) { *plink = (void *)pblk; plink = (void **)pblk; pblk= pblk + blksize; } *plink = (void *)0;
|
为了修改指针的值,使用了二级指针。将一个二级指针指向的指针的值先修改成下一个内存块的起始地址,然后将该二级指针的指向下一个内存块的起始地址,然后循环,以此构建一个指针链表。最后一个指针指向NULL。
3.2 分配一个内存块,OSMemGet()
如果内存分区没有被申请完,OSMemGet()返回一个内存区域里的一个空闲块。将空闲链表里的第一个元素删除即可,其实就是修改.OSMemFreeList的向,将它指向空闲块链表第二个元素就OK。
3.3 释放一个内存块,OSMemPut()
释放内存时,OSMemPut()将改内存块的指针放回空闲队列。将.OSMemFreeList的值指向它,再将第一个元素指向原链表头。这样可以观察到,对于内存的动态申请和释放,UCOSii用的实际上是先进先出的策略。最后被释放的内存块将会最先被分配给申请者。
3.4 查询一个内存分区的状态,OSMemQuery()
返回Mcb当前的各种属性。