µC/OS-II内存管理
µC/OS-II内存管理不是广义上的内存管理,比如程序段的划分法,堆栈的安排等等。它所谓的内存管理仅仅指的是动态申请内存那一部分。换句话说,就是 对应malloc和free函数的内容。它用自己的方式,替代了ANSI C关于malloc,free的方法,号称解决了malloc,free造成的 内存碎片问题。
一.µC/OS-II内存管理概述
我们知道,malloc申请的内存在堆上,由操作系统维护着这个堆。站在操作系统的角度,实际上就是操作系统在分配这一片内存,也许是一个链表,也或是一个数组。不管具体是什么样的数据结构,一定是全局的。
µC/OS-II的内存管理其实就是操作着若干个数组,而且是二维数组。
先摘录教材的一句话,“在µC/OS-II中,操作系统把连续的大块内存按分区来管理。每个分区中包含有整数个大小相同的内存块”。意思是,将内存分为若 干个区,每个区再等分成若干份。每个分区就是个二维数组了,一维长度表示这个分区分成了多少份,二维长度就表示每份有多少字节。至于这若干二维数组是不是 在空间上连续,就不知道了。
光有这些二维数组显然是不够的,还得定义一个数据结构来保存每个数组的使用情况(也就是管理了)。显然,一个二维数组对应一个这样的结构体。内存管理就是围绕这个结构体进行的。
二维数组如何与这个结构体关联上的了?其实,这就是µC/OS-II的高明之处了。内存的申请是用户程序(这里是一个任务),那么每次申请的大小以及可能用到的总大小用户最清楚了,由用户来指定这个二维数组就很合理。由用户来创建一个这样的一片分区,并调用对应函数来完成关联。这样做一是带来了极大的灵活性,二是避免浪费空间。当然缺点也是有的,因为不可能指定无限个这样的二维数组,就不能申请任意大小的内存,应用就会受到很大的限制。
二.µC/OS-II内存管理的数据结构
如果要在μC/OS-II 中使用内存管理,需要在OS_CFG.H文件中将开关量OS_MEM_EN设置为1。
1. 首先,我们来看上面所说的结构体,称之为内存控制块:
#if (OS_MEM_EN > 0) && (OS_MAX_MEM_PART > 0)
typedef struct os_mem{/*MEMORY CONTROL BLOCK */
void *OSMemAddr; /* Pointer to beginning of memory partition */
void *OSMemFreeList; /* Pointer to list of free memory blocks */
INT32U OSMemBlkSize; /* Size (in bytes) of each block of memory */
INT32U OSMemNBlks; /* Total number of blocks in this partition */
INT32U OSMemNFree; /* Number of memory blocks remaining in this partition*/
#if OS_MEM_NAME_SIZE > 1
INT8U OSMemName[OS_MEM_NAME_SIZE]; /* Memory partition name */
#endif
} OS_MEM;
#endif
.OSMemAddr 是指向内存分区起始地址的指针。也就是它对应的二维数组其实地址。
.OSMemFreeList是指向下一个空闲内存控制块或者下一个空闲的内存块的指针。
.OSMemBlkSize是内存分区中内存块的大小,也就是数组的二维长度
.OSMemNBlks 是内存分区中总的内存块数量,也就是数组的一维长度
.OSMemNFree 是内存分区中当前可以得空闲内存块数量。
2. 空闲内存控制块链表
上面说了,一个分区(数组)对应一个OS_MEM,有若干分区就有若干个OS_MEM了。µC/OS-II定了一个OS_MEM数组OSMemTbl。数组长度OS_MAX_MEM_PART由决定。如下:
#if (OS_MEM_EN > 0) && (OS_MAX_MEM_PART > 0)
OS_EXT OS_MEM *OSMemFreeList; /* Pointer to free list of memory partitions */
OS_EXT OS_MEM OSMemTbl[OS_MAX_MEM_PART];/*Storage for memory partition manager*/
#endif
到这里大家可能已经大致明白了,一个内存分区(二维数组)对应OSMemTbl数组的一个元素。定义了OSMemFreeList,表明OSMemTbl数组还被弄成了一个链表。OS_MEM的.OSMemFreeList字段(注意:不是全集变量那个OSMemFreeList,以下也一样)的指向就有两种可能。如果OSMemTbl中某个元素未和具体分区关联时,他就是一个空闲分区控制块,.OSMemFreeList字段它指向下一个空闲内存控制块(同样是没有和具体分区关联的OS_MEM)。如果OSMemTbl中某个元素已经和具体分区关联了,.OSMemFreeList就指向分区内下一个可用的内存块。
3. 分区数据结构
虽然分区是个二维数组,但是还包括一些额外的特性。那就是每个一维数组的开始处保存着下一维的首地址。有点难懂,举例:char A[5][32]; 那么A[0][0]~A[0][3]四个字节保存着A[1][0]的地址(认为地址用4字节表示)。那么A[1][0]~A[1][3]四个字节保存着A[2][0]的地址,以此类推。那么2.2中说的.OSMemFreeList字段就是指向A[0][0]/A[1][0]/A[2][0]/A[3][0]/A[4][0]某一个的地址了。
三.µC/OS-II内存管理具体分析
1.OSMemTb的初始化
void OS_MemInit (void)
{
#if OS_MAX_MEM_PART == 1
OS_MemClr((INT8U *)&OSMemTbl[0], sizeof(OSMemTbl)); /* Clear the memory partition table */
OSMemFreeList = (OS_MEM *)&OSMemTbl[0]; /* Point to beginning of free list */
#if OS_MEM_NAME_SIZE > 1
OSMemFreeList->OSMemName[0] = '?'; /* Unknown name */
OSMemFreeList->OSMemName[1] = OS_ASCII_NUL;
#endif
#endif
#if OS_MAX_MEM_PART >= 2
OS_MEM *pmem;
INT16U i;
OS_MemClr((INT8U *)&OSMemTbl[0], sizeof(OSMemTbl)); /* Clear the memory partition table */
pmem = &OSMemTbl[0]; /* Point to memory control block (MCB) */
for (i = 0; i < (OS_MAX_MEM_PART - 1); i++) { /* Init. list of free memory partitions */
pmem->OSMemFreeList = (void *)&OSMemTbl[i+1]; /* Chain list of free partitions */
#if OS_MEM_NAME_SIZE > 1
pmem->OSMemName[0] = '?'; /* Unknown name */
pmem->OSMemName[1] = OS_ASCII_NUL;
#endif
pmem++;
}
pmem->OSMemFreeList = (void *)0; /* Initialize last node */
#if OS_MEM_NAME_SIZE > 1
pmem->OSMemName[0] = '?'; /* Unknown name */
pmem->OSMemName[1] = OS_ASCII_NUL;
#endif
OSMemFreeList = &OSMemTbl[0]; /* Point to beginning of free list */
#endif
}
#endif
这是初始化的函数。第18,19行就是将数组建成一个链表。第22,23行是将内存控制块命名的,这个不是必须的。结果如图所示
2.建立一个分区
先贴出建立分区的函数,再进行分析。
OS_MEM *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err)
{
OS_MEM *pmem;
INT8U *pblk;
void **plink;
INT32U i;
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
#if OS_ARG_CHK_EN > 0
if (err == (INT8U *)0) { /* Validate 'err' */
return ((OS_MEM *)0);
}
if (addr == (void *)0) { /* Must pass a valid address for the memory part.*/
*err = OS_MEM_INVALID_ADDR;
return ((OS_MEM *)0);
}
if (((INT32U)addr & (sizeof(void *) - 1)) != 0){ /* Must be pointer size aligned */
*err = OS_MEM_INVALID_ADDR;
return ((OS_MEM *)0);
}
if (nblks < 2) { /* Must have at least 2 blocks per partition */
*err = OS_MEM_INVALID_BLKS;
return ((OS_MEM *)0);
}
if (blksize < sizeof(void *)) { /* Must contain space for at least a pointer */
*err = OS_MEM_INVALID_SIZE;
return ((OS_MEM *)0);
}
if ((blksize % sizeof(void *)) != 0) { /* Must contain space for an integral number ... */
*err = OS_MEM_INVALID_SIZE; /* ... of pointer sized items */
return ((OS_MEM *)0);
}
#endif
OS_ENTER_CRITICAL();
pmem = OSMemFreeList; /* Get next free memory partition */
if (OSMemFreeList != (OS_MEM *)0) { /* See if pool of free partitions was empty */
OSMemFreeList = (OS_MEM *)OSMemFreeList->OSMemFreeList;
}
OS_EXIT_CRITICAL();
if (pmem == (OS_MEM *)0) { /* See if we have a memory partition */
*err = OS_MEM_INVALID_PART;
return ((OS_MEM *)0);
}
plink = (void **)addr; /* Create linked list of free memory blocks */
pblk = (INT8U *)((INT32U)addr + blksize);
for (i = 0; i < (nblks - 1); i++) {
*plink = (void *)pblk; /* Save pointer to NEXT block in CURRENT block */
plink = (void **)pblk; /* Position to NEXT block */
pblk = (INT8U *)((INT32U)pblk + blksize); /* Point to the FOLLOWING block */
}
*plink = (void *)0; /* Last memory block points to NULL */
pmem->OSMemAddr = addr; /* Store start address of memory partition */
pmem->OSMemFreeList = addr; /* Initialize pointer to pool of free blocks */
pmem->OSMemNFree = nblks; /* Store number of free blocks in MCB */
pmem->OSMemNBlks = nblks;
pmem->OSMemBlkSize = blksize; /* Store block size of each memory blocks */
*err = OS_NO_ERR;
return (pmem);
}
addr就是所说的分区(二维数组)首地址。nblks就是一维长度,即有多少块;blksize是二维长度,即每块多大。err是用来保存错误代码的。第7行到34行是错误代码检测,大家都能看懂。第37行至40行,就是在上一节所描述的内存控制块链表中找出一个空闲块,然后将空闲指针往后移动下一个空闲内存控制块。下面着重介绍第46行到60行。为了方便说明,我们假定一个addr就是char A[6][32]的首地址。
plink是二维指针,先指向A[0][0], 说明A[0][0]开始保存的内容是一个指针(46行)。接着,pblk获取A[1][0]的地址,因为偏移了blksize也就是32个地址,超过了A[0][31](47行)。接下来,就将A[1][0]的地址保存到A[0][0]开始的4字节(49行),将plink重新指向A[1][0],同上(50行),pblk再获取A[2][0]的地址(51行)。然后循环,直到将二维数组串联完。这里可能对plink这个二维指针不好理解。那是因为每块开始处要保存的内容是指针,那么指向它的就必须是二维指针。如果不用二维指针也,可以参照47行的形式,将addr先转换为4字节长度数据类型的指针,比如INT32U *p = (INT32U *)addr; *p = (INT32U)&A[1][0].显然这种方式,更加费解。
第54至58行,就是将分块信息填到对应的OS_MEM结构中。后面内存的申请和释放就根据这个结构体保存的信息来操作了。这个结构体的地址就是函数的还回值。
结果如图所示:
3. 内存申请 OSMemGet函数。
void *OSMemGet (OS_MEM *pmem, INT8U *err)
{
void *pblk;
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
#if OS_ARG_CHK_EN > 0
if (err == (INT8U *)0) { /* Validate 'err' */
return ((void *)0);
}
if (pmem == (OS_MEM *)0) { /* Must point to a valid memory partition */
*err = OS_MEM_INVALID_PMEM;
return ((void *)0);
}
#endif
OS_ENTER_CRITICAL();
if (pmem->OSMemNFree > 0) { /* See if there are any free memory blocks */
pblk = pmem->OSMemFreeList; /* Yes, point to next free memory block */
pmem->OSMemFreeList = *(void **)pblk; /* Adjust pointer to new free list */
pmem->OSMemNFree--; /* One less memory block in this partition */
OS_EXIT_CRITICAL();
*err = OS_NO_ERR; /* No error */
return (pblk); /* Return memory block to caller */
}
OS_EXIT_CRITICAL();
*err = OS_MEM_NO_FREE_BLKS; /* No, Notify caller of empty memory partition */
return ((void *)0); /* Return NULL pointer to caller */
}
函数的参数就是
OSMemCreate
函数的返回值。错误检测代码就不用解释了。首先需要检查是否还有未分配的块(
18
),如果有,获取空闲块首地址
(19
行
)
,修改
OSMemFreeList
空闲块指针(
20
行)。原理与
OSMemCreate
函数里面相同,不解释。接着,将空闲块计数器减
1
(
21
行),然后还回获取的空闲块的首地址。当然没有可用块,还回
0.
4. 释放内存函数OSMemPut
INT8U OSMemPut (OS_MEM *pmem, void *pblk)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
#if OS_ARG_CHK_EN > 0
if (pmem == (OS_MEM *)0) { /* Must point to a valid memory partition */
return (OS_MEM_INVALID_PMEM);
}
if (pblk == (void *)0) { /* Must release a valid block */
return (OS_MEM_INVALID_PBLK);
}
#endif
OS_ENTER_CRITICAL();
if (pmem->OSMemNFree >= pmem->OSMemNBlks) { /* Make sure all blocks not already returned */
OS_EXIT_CRITICAL();
return (OS_MEM_FULL);
}
*(void **)pblk = pmem->OSMemFreeList; /* Insert released block into free block list */
pmem->OSMemFreeList = pblk;
pmem->OSMemNFree++; /* One more memory block in this partition */
OS_EXIT_CRITICAL();
return (OS_NO_ERR); /* Notify caller that memory block was released */
}
函数的第一个参数就是OSMemCreate的还回值,第二参数是OSMemGet的还回值。首先需要检查分区是否还未被申请(16),如果是,还回错误。因为分区都没有被申请过,何来的释放了?如果不是,获取当前OSMemFreeList空闲块指针指向非的地址并赋给pblk开始处,(20行)修改OSMemFreeList空闲块指针指向pblk(21行)。原理与OSMemCreate函数里面相同,不解释。接着,将空闲块计数器减加(22行)。
5. 其他函数
查询内存控制块名称的OSMemNameGet函数,设置内存控制块名称的OSMemNameSet函数,查询内存控制块详细参数的OSMemQuery函数。大家都能看懂,就不做介绍。
四.小结
虽然µC/OS-II的内存管理号称解决了malloc的内存碎片问题,却是牺牲了可随意申请大小的方便。实际上,malloc也是用内存链表的方式来管理的,和这个并没有多大的区别。知道的同学可以在后面回复。另外,µC/OS-II适用的一般都是比较小的系统,所以让用户来创建分块是可行的。其实,用户完全可以自己特有的方式来管理那一个数组,而不用系统提供的这个几个函数。当然,如果没有特殊的要求,用这个可以减轻开发负担。