【UCOSii源码解析】内存管理

系列文章

  1. UCOSii启动流程以及平台相关文件分析
  2. 优先级算法及内核源码分析
  3. 任务管理
  4. 时间管理
  5. 事件控制块
  6. 内存管理
  7. 任务间通讯与同步

在这里插入图片描述

(一)内存管理算法

在ANSI C中可以用malloc()和free()两个函数动态地分配内存和释放内存。但是,在嵌入式实时操作系统中,多次这样做会把原来很大的一块连续内存区域,逐渐地分割成许多非常小而且彼此又不相邻的内存区域,也就是内存碎片。由于这些碎片的大量存在,使得程序到后来连非常小的内存也分配不到。
在嵌入式设备中,持续的调用malloc()和free()容易产生内存碎片,长时间的运行最终会导致内存消耗殆尽。UCOS提供了一套内存管理机制,在系统初始化的时候就分配好内存空间,将所有可用的空间组织成链表,需要申请内存的时候直接从链表中申请,释放内存的时候直接将内存归还到空余内存链表中即可。使用这种方法不仅避免了内存碎片的产生,而且使得在常数时间内分配内存空间成为可能。

所以说UCOS中的内存管理算法与标准C中有啥区别嘞

1、函数原型及说明:

void *malloc(long NumBytes):该函数分配了NumBytes个字节,并返回了指向这块内存的指针。如果分配失败,则返回一个空指针(NULL)。

关于分配失败的原因,应该有多种,比如说空间不足就是一种。

void free(void *FirstByte): 该函数是将之前用malloc分配的空间还给程序或者是操作系统,也就是释放了这块内存,让它重新得到自由。

2、 C语言malloc算法产生内存碎片的原因

假设有一块一共有100个单位的连续空闲内存空间,范围是099.如果你从中申请一块内存,如10个单位,那么申请出来的内存块就为09区间.这时继续申请一块内存,比如说5个单位大,第二块得到的内存块就应该为1014区间.如果把第一块内存块释放,然后再申请一块大于10个单位的内存块,比如说20个单位.因为刚被释放的内存块不能满足新的请求,所以只能从15开始分配出20个单位的内存块.现在整个内存空间的状态是09空闲,1014被占用,1524被占用,2599空闲。其中09就是一个内存碎片了.如果1014一直被占用,而以后申请的空间都大于10个单位,那么09就永远用不上了,造成内存浪费.。

这么搞对于内存足够的设备当然没啥问题,但是当你内存蹦来就不大的时候,移植运行下去,就会产生申请不出内存的情况。那么UCOS是怎么避免这种情况的呢。

3 、核心变量

有一个全局的变量OSMemFreeList

#if (OS_MEM_EN > 0u) && (OS_MAX_MEM_PART > 0u)
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数组中存放着所有内存控制块结点。

(二)内存控制块

typedef struct os_mem      //内存控制块
{                     
    void   *OSMemAddr;     //指向内存分区的首地址
    void   *OSMemFreeList; //该内存分区的block链表的表头
    INT32U  OSMemBlkSize;  //每一个block的大小
    INT32U  OSMemNBlks;    //该分区中block的数目
    INT32U  OSMemNFree;    //该分区中空闲block的数目
} OS_MEM;

.OSMemAddr是指向内存分区起始地址的指针。它在建立内存分区建立一个内存分区,OSMemCreate()]时被初始化,在此之后就不能更改了。
.OSMemFreeList是指向下一个空闲内存控制块或者下一个空闲的内存块的指针,具体含义要根据该内存分区是否已经建立来决定。
.OSMemBlkSize是内存分区中内存块的大小,是用户建立该内存分区时指定的。
.OSMemNBlks是内存分区中总的内存块数量,也是用户建立该内存分区时指定的。
.OSMemNFree是内存分区中当前可以得空闲内存块数量。

所以每当申请一次内存的时候就会产生一个内存控制块,一个内存分区被划分成很多个大小相等的内存块,这些内存块链接成链表,链表的表头存于OSMemFreeList中。

在这里插入图片描述

比如一个有4个内存块,每个内存块128bit的内存分区Memoy如下:
INT32U Memory[4][4]
UCOS的思路是我们将要分配的空间组织成二维数组,然后二维数组的每一行就是一个block,二维数组的列数既是block的大小。
Memory的第一个block的首地址是 Memor[0]
Memory的第二个block的首地址是 Memory[1]
Memory的第三个block的首地址是 Memory[2]
Memory的第四个block的首地址是 Memory[3]
为了管理方便我们在每一个block的开头存储下一个block的地址,这样就把所有的block串接成了单链表。
第一个block的开头存储 Memory[1]
第一个block的开头存储 Memory[2]
第一个block的开头存储 Memory[3]
第一个block的开头存储 NULL

在这里插入图片描述

(三)源码分析

内存管理初始化函数

258 *************************************************************************************************
259 */
260
261 void OS_MemInit (void) //初始化内存分区
262 {
263 #if OS_MAX_MEM_PART == 1 //最多内存块的数目为1时
264 OSMemFreeList = (OS_MEM *)&OSMemTbl[0]; //内存块链接表=内存块首地址
265 OSMemFreeList->OSMemFreeList = (void *)0; //内存块链接表=0
266 OSMemFreeList->OSMemAddr = (void *)0; //内存区起始地址的指针=0
267 OSMemFreeList->OSMemNFree = 0; //空闲的内存块数目=0
268 OSMemFreeList->OSMemNBlks = 0; //该内存区的内存块总数=0
269 OSMemFreeList->OSMemBlkSize = 0; //内存块的大小=0
270 #endif
271
272 #if OS_MAX_MEM_PART >= 2 //最多内存块的数目为多个时
273 OS_MEM *pmem; //定义内存区控制块的指针
274 INT16U i; //定义分区的内存数量
275
276
277 pmem = (OS_MEM *)&OSMemTbl[0]; //内存区控制块的指针=内存控制块(MCB)首地址
278 for (i = 0; i < (OS_MAX_MEM_PART - 1); i++) { //设定循环初始化(i次)
279 pmem->OSMemFreeList = (void *)&OSMemTbl[i+1]; //内存块链接表=内存块地址(对应的分区)
280 pmem->OSMemAddr = (void *)0; //内存区起始地址的指针=0
281 pmem->OSMemNFree = 0; //空闲的内存块数目=0
282 pmem->OSMemNBlks = 0; //该内存区的内存块总数=0
283 pmem->OSMemBlkSize = 0; //内存块的大小=0
284 pmem++;
285 }
286 pmem->OSMemFreeList = (void *)0; //初始化最后的内存块链接表
287 pmem->OSMemAddr = (void *)0; //内存区起始地址的指针=0
288 pmem->OSMemNFree = 0; //空闲的内存块数目=0
289 pmem->OSMemNBlks = 0; //该内存区的内存块总数=0
290 pmem->OSMemBlkSize = 0; //内存块的大小=0
291
292 OSMemFreeList = (OS_MEM *)&OSMemTbl[0]; //回到开始的内存块链接表
293 #endif
294 }
295 #endif //OS_MEM.C文件结束

创建内存分区

20 *************************************************************************************************
21 * 建立并初始化一块内存区(CREATE A MEMORY PARTITION)
22 *
23 * 描述: 建立并初始化一块内存区。一块内存区包含指定数目的大小确定的内存块。程序可以包含这些内存
24 * 块并在用完后释放回内存区。
25 *
26 * 参数: addr 建立的内存区的起始地址。内存区可以使用静态数组或在初始化时使用malloc()函数建立。
27 *
28 * nblks 需要的内存块的数目。每一个内存区最少需要定义两个内存块。
29 *
30 * blksize 每个内存块的大小,最少应该能够容纳一个指针。
31 *
32 * err 是指向包含错误码的变量的指针。OSMemCreate()函数返回的错误码可能为下述几种:
33 * OS_NO_ERR 成功建立内存区;
34 * OS_MEM_INVALID_ADDR 非法地址,即地址为空指针;
35 * OS_MEM_INVALID_PART 没有空闲的内存区;
36 * OS_MEM_INVALID_BLKS 没有为每一个内存区建立至少2个内存块;
37 * OS_MEM_INVALID_SIZE 内存块大小不足以容纳一个指针变量。
38 * 返回: 返回指向内存区控制块的指针。如果没有剩余内存区,OSMemCreate()函数返回空指针。
39 *
40 * 注意: 必须首先建立内存区,然后使用
41 *************************************************************************************************
42 */
43 //建立并初始化一块内存区(起始地址、需要的内存块数目、每块内存块大小、返回错误的指针)
44 OS_MEM *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err)
45 {
46 #if OS_CRITICAL_METHOD == 3 //中断函数被设定为模式3
47 OS_CPU_SR cpu_sr;
48 #endif
49 OS_MEM *pmem; //内存控制块指针
50 INT8U *pblk; //每块内存块的起始地址
51 void **plink; //链接起始地址
52 INT32U i; //内存包含的内存区数量
53
54
55 #if OS_ARG_CHK_EN > 0 //所有参数在指定的范围之内
56 if (addr == (void *)0) { //当内存起始地址为0时
57 *err = OS_MEM_INVALID_ADDR; //错误显示为(非法地址,即地址为空指针,无效)
58 return ((OS_MEM *)0);
59 }
60 if (nblks < 2) { //每个内存分区至少有两个内存块
61 *err = OS_MEM_INVALID_BLKS; //否则显示(没有为每一个内存区建立至少2个内存块)
62 return ((OS_MEM *)0);
63 }
64 if (blksize < sizeof(void *)) { //每个内存块至少容得一个指针(链接指针)
65 *err = OS_MEM_INVALID_SIZE; //否则显示(内存块大小不足以容纳一个指针变量)
66 return ((OS_MEM *)0);
67 }
68 #endif
69 OS_ENTER_CRITICAL(); //关闭中断
70 pmem = OSMemFreeList; //内存控制块指针=空余内存控制块(链接)
71 if (OSMemFreeList != (OS_MEM *)0) { //当内存链接控制块≠0,即有空余控制块
72 OSMemFreeList = (OS_MEM *)OSMemFreeList->OSMemFreeList; //指向下一个空余链接控制块
73 }
74 OS_EXIT_CRITICAL(); //打开中断
75 if (pmem == (OS_MEM *)0) { //判断是否有空余内存控制块(为1有)
76 *err = OS_MEM_INVALID_PART; //没有空闲的内存区
2 H:\SOURCE中文源代码\OS_MEM.C
77 return ((OS_MEM *)0); //返回Null,建立内存失败
78 }
79 plink = (void **)addr; //链接起始地址=内存分区起始地址
80 pblk = (INT8U *)addr + blksize; //每块内存的起始地址=内存分区起始地址+每块内存块大小
81 for (i = 0; i < (nblks - 1); i++) { //循环体(需要的内存块数目(次数))?
82 *plink = (void *)pblk; //链接起始地址(内容)=每块内存块的起始地址(内容)?
83 plink = (void **)pblk; //链接起始地址=内存块的起始地址指针(内容)?
84 pblk = pblk + blksize; //内存块的起始地址=自己+每块内存块大小?
85 }
86 *plink = (void *)0; //链接起始地址=0
87 OS_ENTER_CRITICAL(); //关闭中断
88 pmem->OSMemAddr = addr; //内存区指针=分区起始地址
89 pmem->OSMemFreeList = addr; //下一空余控制块=分区起始地址
90 pmem->OSMemNFree = nblks; //分区中内存块大小=需要的内存块数目
91 pmem->OSMemNBlks = nblks; //总的内存块数量=需要的内存块数目
92 pmem->OSMemBlkSize = blksize; //空余内存块数量=每块内存块大小
93 OS_EXIT_CRITICAL(); //打开中断
94 *err = OS_NO_ERR; //成功建立内存区
95 return (pmem); //返回(内存控制块指针)
96 }

申请内存

97 /*$PAGE*/?
98 /*
99 *************************************************************************************************
100 * 从内存区分配一个内存块(GET A MEMORY BLOCK)
101 *
102 * 描述: 用于从内存区分配一个内存块。用户程序必须知道所建立的内存块的大小,同时用户程序必须在使
103 * 用完内存块后释放内存块。使用OSMemGet()函数释放内存块。可以多次调用OSMemGet()函数。
104 *
105 * 参数: pmem 是指向内存区控制块的指针,可以从OSMemCreate()函数返回得到。
106 *
107 * err 是指向包含错误码的变量的指针。OSMemGet()函数返回的错误码可能为下述几种:
108 * OS_NO_ERR 成功得到一个内存块;
109 * OS_MEM_NO_FREE_BLKS 内存区已经没有空间分配给内存块;
110 * OS_MEM_INVALID_PMEM 'pmem'是空指针。
111 *
112 * 返回: 返回指向内存区块的指针。如果没有空间分配给内存块,OSMemGet()函数返回空指针。
113 *
114 * 注意: 必须首先建立内存区,然后使用
115 *************************************************************************************************
116 */
117
118 void *OSMemGet (OS_MEM *pmem, INT8U *err) //从内存区分配一个内存块(内存区控制块的指针、错误指针)
119 {
120 #if OS_CRITICAL_METHOD == 3 //中断函数被设定为模式3
121 OS_CPU_SR cpu_sr;
122 #endif
123 void *pblk; //内存块的起始地址
124
125 #if OS_ARG_CHK_EN > 0 //所有的参数都是在指定的范围内
126 if (pmem == (OS_MEM *)0) { //指向内存区控制块的指针不能为空指针
127 *err = OS_MEM_INVALID_PMEM; //'pmem'是空指针
128 return ((OS_MEM *)0); //返回Null
129 }
130 #endif
131 OS_ENTER_CRITICAL(); //关闭中断
132 if (pmem->OSMemNFree > 0) { //检查内存分区中是否有空余的内存块(必须大于0)
133 pblk = pmem->OSMemFreeList; //刷新空余内存块链接表
134 pmem->OSMemFreeList = *(void **)pblk; //将链接表头指针后移1个元素
135 pmem->OSMemNFree--; //将空余内存块数减1
136 OS_EXIT_CRITICAL(); //打开中断
137 *err = OS_NO_ERR; //成功得到一个内存块
138 return (pblk); //返回(内存块的起始地址)
139 }
140 OS_EXIT_CRITICAL(); //打开中断
141 *err = OS_MEM_NO_FREE_BLKS; //内存区已经没有空间分配给内存块
142 return ((void *)0); //返回(没有空间分配给内存块)
143 }

释放内存

146 *************************************************************************************************
147 * 释放一个内存块 (RELEASE A MEMORY BLOCK)
148 *
149 * 描述: 释放一个内存块,内存块必须释放回原先申请的内存区。
150 *
151 * 参数: pmem 是指向内存区控制块的指针,可以从OSMemCreate()函数 返回得到。
153 * pblk 是指向将被释放的内存块的指针。
154 *
155 * 返回: OS_NO_ERR 成功释放内存块;
156 * OS_MEM_FULL 内存区已经不能再接受更多释放的内存块。这种情况说明用户程序出现
157 * 了错误,释放了多于用OSMemGet()函数得到的内存块
158 * OS_MEM_INVALID_PMEM 'pmem'是空指针;
159 * OS_MEM_INVALID_PBLK //指向将被释放的内存块的指针不能为空指针
160 *
161 * 注意: 1)必须首先建立内存区,然后使用;
162 * 2)内存块必须释放回原先申请的内存区。
163 *************************************************************************************************
164 */
165
166 INT8U OSMemPut (OS_MEM *pmem, void *pblk)
167 { //释放一个内存块(内存区控制块的指针、被释放的内存块的指针)
168 #if OS_CRITICAL_METHOD == 3 //中断函数被设定为模式3
169 OS_CPU_SR cpu_sr;
170 #endif
171
172
173 #if OS_ARG_CHK_EN > 0 //所有的参数都是在指定的范围内
174 if (pmem == (OS_MEM *)0) { //指向内存区控制块的指针不能为空指针
175 return (OS_MEM_INVALID_PMEM); //'pmem'是空指针
176 }
177 if (pblk == (void *)0) { //指向将被释放的内存块的指针不能为空指针
178 return (OS_MEM_INVALID_PBLK); //'pblk'是空指针
179 }
180 #endif
181 OS_ENTER_CRITICAL(); //关闭中断
182 if (pmem->OSMemNFree >= pmem->OSMemNBlks) { //分区中内存块大小 >= 总的内存块数量(已满)
183 OS_EXIT_CRITICAL(); //打开中断
184 return (OS_MEM_FULL); //内存区已经不能再接受更多释放的内存块
185 }
186 //如果未满,将释放的内存块插入到该分区的空余内存块链接表中
187 *(void **)pblk = pmem->OSMemFreeList;
188 pmem->OSMemFreeList = pblk; //将链接表头指针指向被释放的内存块的指针
189 pmem->OSMemNFree++; //将分区中空余的内存块总数加1
190 OS_EXIT_CRITICAL();
191 return (OS_NO_ERR); //成功释放内存块
192 }
193 /*$PAGE*/

(四)程序示例:等待从一个内存分区中分配内存块

OS_EVENT  *SemaphorePtr;	                                        (1)
OS_MEM    *PartitionPtr;
INT8U      Partition[100][32];
OS_STK     TaskStk[1000];


void main (void)
{
    INT8U err;

    OSInit();	                                                   (2)
    .
    .
    SemaphorePtr = OSSemCreate(100);	                            (3)
    PartitionPtr = OSMemCreate(Partition, 100, 32, &err);	       (4)
    .
    OSTaskCreate(Task, (void *)0, &TaskStk[999], &err);	         (5)
    .
    OSStart();	                                                  (6)
}
void Task (void *pdata)
{
    INT8U  err;
    INT8U *pblock;


    for (;;) {
        OSSemPend(SemaphorePtr, 0, &err);	                       (7)
        pblock = OSMemGet(PartitionPtr, &err);	                  (8)
        .
        .  /* 使用内存块 */
        .
        OSMemPut(PartitionPtr, pblock);	                         (9)
        OSSemPost(SemaphorePtr);	                               (10)
    }
}

(五)基于查表法实现的动态内存管理

还记得之前学习STM32的时候原子哥也自己写了一个内存管理的算法,现在将这两种算法做一下比对。

在这里插入图片描述

从上图可以看出,分块式内存管理由内存池和内存管理表两部分组成。内存池被等分为 n块,对应的内存管理表,大小也为 n,内存管理表的每一个项对应内存池的一块内存。内存管理表的项值代表的意义为:当该项值为 0 的时候,代表对应的内存块未被占用,当该项值非零的时候,代表该项对应的内存块已经被占用,其数值则代表被连续占用的内存块数。比如某项值为 10,那么说明包括本项对应的内存块在内,总共分配了 10 个内存块给外部的某个指针。内寸分配方向如图所示,是从顶底的分配方向。(即从高位地址到低位地址)即首先从最末端开始找空内存。当内存管理刚初始化的时候,内存表全部清零,表示没有任何内存块被占用。
分配原理
当指针 p 调用 malloc 申请内存的时候,先判断 p 要分配的内存块数( m),然后从第 n 项开始,向下查找,直到找到 m 块连续的空内存块(即对应内存管理表项为 0),然后将这 m 个内存管理表项的值都设置为 m(标记被占用),最后,把最后的这个空内存块的地址返回指针 p,完成一次分配。注意,如果当内存不够的时候(找到最后也没找到连续的 m 块空闲内存),则返回 NULL 给 p,表示分配失败。
释放原理
当 p 申请的内存用完,需要释放的时候,调用 free 函数实现。 free 函数先判断 p 指向的内存地址所对应的内存块,然后找到对应的内存管理表项目,得到 p 所占用的内存块数目 m(内存管理表项目的值就是所分配内存块的数目),将这 m 个内存管理表项目的值都清零,标记释放,完成一次内存释放。

以前不懂以为正点原子重写内存分配是为了防止内存碎片, 现在一看,WC这和标准C有什么区别。你找连续的为0的数,不就代表着会产生小的内存碎片吗。所以说还是UCOS的链表法真正做到了防止碎片的产生。

那怎么解决这个问题呢,我在网上找到一个大神的代码,他是这样解决的。


void *mymallocrun(MEM_UINT i,MEM_UINT size)
{
		MEM_UINT m = 0,n =0,s=0;	
		MEM_UINT temp_size=0;
		void * temp_addr=0;
	
		if(size < MEM->free_table[i].length)					//判断申请字节小于可用空间字节
		{
			n = i+1;  //计算下一个空闲分区表
			
			if(n < MAX_BLOCK)
			{	
				if(MEM->endtablep+1==i)  //新内存分配
				{
					s=n;		
				
					temp_addr = MEM->free_table[i].address;
					temp_size =  MEM->free_table[i].length;
						
					MEM->free_table[s].address = (void *)((MEM_ADD)temp_addr+size);
					MEM->free_table[s].length = temp_size - size;
					
				
					MEM->free_table[i].length = size;		
					MEM->free_table[i].flag  = 1;	
					MEM->endtablep = i;							//记录表指针位置	

					return (void*)(MEM->free_table[i].address);					//返回地址				
				}//下边为以前释放的内存块管理
				else if(MEM->endtablep == END_BLOCK)//当前记录表为末端表无法往后移动一个分区表
				{					
					MEM->free_table[i].flag = 1;	
		
					return (void*)(MEM->free_table[i].address);					//返回地址
				}
				else if((MEM->endtablep+1) == END_BLOCK) //末端表为空禁止往后移动一个分区表(防止内存溢出)
				{					
					MEM->free_table[i].flag = 1;	
		
					return (void*)(MEM->free_table[i].address);					//返回地址
				}
				else
				{													
					m = MEM->endtablep+1;							
					
					while(m >= n)
					{
						MEM->free_table[m+1].address = MEM->free_table[m].address;	//将地址往后移动一个分区表

						MEM->free_table[m+1].length = MEM->free_table[m].length;		//将数据长度移动一个分区表

						MEM->free_table[m+1].flag  = MEM->free_table[m].flag ;		  //将标志移动一个分区表
						
						m--;
					}			
					temp_size = MEM->free_table[i].length;

					MEM->free_table[i].length = size;
					MEM->free_table[i].flag = 1;	
				
					MEM->free_table[n].address = (void*)((MEM_ADD)MEM->free_table[i].address + MEM->free_table[i].length);
					MEM->free_table[n].length = temp_size - size;
					MEM->free_table[n].flag = 0;
					MEM->endtablep += 1;		//记录表指针位置			

					return (void*)(MEM->free_table[i].address);					//返回地址
				}	
			}	
			else
			{		
				 //当前记录表为末端表			
				MEM->free_table[i].flag = 1;
				MEM->endtablep = END_BLOCK;		//记录结束表指针位置						
		
				cout<<"已到记录表末端"<<n<<"\n";
				return (void*)(MEM->free_table[i].address);					//返回地址							
			}						
		}
		else  //当前空间与申请空间相等
		{	
			MEM->free_table[i].flag  = 1;	
			
			if(MEM->endtablep < i){MEM->endtablep = i;}	//记录表指针位置	

			return (void*)(MEM->free_table[i].address);					//返回地址			
		}									
}

就是每当内存分区不够用的时候,可以采用这样的方式,将整个内存分区整理一下,消灭小的碎片内存。但是这样真的有链表方便吗。头皮一阵发麻。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

与光同程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值