学习目标:rt_thread中的内存管理
学习内容:
由于实时系统对于时间的要求,内存分配往往要比通用操作要求苛刻的多。
1.首先分配内存的时间必须是确定的。一般的内存管理算法是根据需要存储的数据的长度在内存中
去寻找一个与这段数据相适应的空闲内存块,然后将数据内存存在里面。而寻找这样一个空闲的内存块,
所耗费的时间是不确定的,因而对于实时操作系统而言,这是不可接受的。
实时系统必须要保证内存块的分配过程在可预测的时间内完成,否则实时任务对外部事件额响应也将变得不可预测。
2.随着内存被不断的分配和释放,将出现内存碎片化的问题,即使系统中还有剩余内存,但由于地址不连续,因而也是无法形成
一块完整的内存分配。所以某些系统需要重启自己的设备,复位内存空间,从而减少系统卡顿。但是对于不允许关机的系统而言,这
是无法接受的。
3.嵌入式系统的资源环境也是不尽相同,有些系统的资源比较紧张,只有数十KB
的内存可供分配,而有些系统则存在数MB的内存,如何为这些不同的系统,选择适合
它们的高效率的内存分配算法,就将变得复杂化。
静态内存池管理:
内存池(memory pool)是一种用于分配大量大小相同的小对象技术,它可以极大地加快内存分配和释放的速度;
内存池在建立的过程中首先先相系统申请一块大的内存,然后分成同样大小的多块内存,小内存块直接通过链表
连接起来(此链表也称之为空闲链表),每次分配的时候,从空闲链表中取出链表头上的第一个内存块提供给申请者。
(见上图)物理内存中允许存在多个大小不同的内存池,每一个内存池又由多个空闲内存块组成,内核用它们来进行内存管理。当一个内
存池对象被创建时,内存池对象就被分配给了一个内存池控制块,内存控制块的参数包括内存池名,内存缓冲区,内存块大小,块数以及一个等待线程队列。
静态内存控制块:
struct rt_mempool
{ struct rt_object parent;
void *start_address;//内存池数据开始地址
rt_size_t size; //内存池数据区域大小
rt_size_t block_size; //内存块大小
rt_uint8_t *block_list //内存块列表
/*内存池区域中可以容纳的最大的内存块数
rt_size_t block_total_count;
/*内存池中空闲的内存块数
rt_size_t block_free_count;
/因为内存块不可用而挂起的线程列表
rt_list_t suspend_thread;
/*因为内存块不可用而挂起的线程数;
rt_size_t suspend_thread_count;
};
typedef struct rt_mempool* rt_mp_t;
每一个内存池对象由上述结构组成,其中suspend_thread形成了一个申请线程等待
列表,即当内存池中无可用内存块,并且申请线程允许等待时,申请线程将挂起在suspend_
thread链表上。
静态内存池接口:
创建内存池操作将会创建一个内存池对象并从堆上分配一个内存池。创建内存池是从对
应内存池中分配和释放内存块的先决条件,创建内存池后,线程便可以从内存池中执行申
请、释放等操作。创建内存池使用下面的函数接口,该函数返回一个已创建的内存池对象。
rt_mp_t rt_mp_create(const char * name,rt_size_t block_count,rt_size_t block_size)
删除内存池将删除内存池对象并释放申请的内存。使用下面的函数接口:
rt_err_t rt_mp_delete(rt_mp_t mp);
删除内存池时,会首先唤醒等待在该内存池对象上的所有线程(返回-RT_ERROR),然
后再释放已从内存堆上分配的内存池数据存放区域,然后删除内存池对象。
初始化内存池:
初始化内存池跟创建内存池类似,只是初始化内存池用于静态内存管理模式,内存池控
制块来源于用户在系统中申请的静态对象。另外与创建内存池不同的是,此处内存池对象所
使用的内存空间是由用户指定的一个缓冲区空间,用户把缓冲区的指针传递给内存池对象控
制块
rt_err_t rt_mp_init(rt_mp_t mp, const char* name, void *start, rt_size_t size, rt_size_t block size);
初始化内存池时,把需要进行初始化的内存池对象传递给内核,同时需要传递的还有内
存池用到的内存空间,以及内存池管理的内存块数目和块大小,并且给内存池指定一个名
称。这样,内核就可以对该内存池进行初始化,将内存池用到的内存空间组织成可用于分配
的空闲块链表。
函数参数
参数描述
mp 内存池对象;
name 内存池名;
start 内存池的起始位置;
size 内存池数据区域大小;
block_size 内存块容量。
函数返回
初始化成功返回RT_OK;否则返回-RT_ERROR。
脱离内存池
脱离内存池将把内存池对象从内核对象管理器中删除。脱离内存池使用下面的函数接
口:
rt_err_t rt_mp_detach(rt_mp_t mp);
分配内存块
从指定的内存池中分配一个内存块,使用如下接口:
void *rt_mp_alloc (rt_mp_t mp, rt_int32_t time);
看的我有点乱,走个例子吧
内存池历程
该程序会创建一个静态的内存池对象,2个动态线程
两个线程会试图从内存池中获得内存块
#include <rtthread.h>
#include"tc_com.h"
static rt_uint8_t *ptr[48]
static rt_uint8_t mempool[4096];
static struct rt_mempool mp;/*静态内存池对象
/*指向线程控制块的指针
static rt_thread_t tid1=RT_NULL;
static rt_thread_t tid2=RT_NULL;
/*线程1入口*/
static void thread1_entry(void* parameter)
{
int i;
char *block;
while(1)
{
for(i=0;i<48;i++)
{ /*申请内存块*/
rt_kprintf(" allocate No.%d\n",i);
if(ptr[i] == RT_MULL)
{ptr[i] = rt_mp_alloc(&mp,RT_WAITING_FOREVER);
}
/* 继续申请一个内存块,因为已经没有内存块,线程应该被挂起*/
block = rt_mp_alloc(&mp, RT_WAITING_FOREVER);
rt_kprintf("allocate the block mem\n");
/* 释放这个内存块*/
rt_mp_free(block);
block = RT_NULL;
}
}
}
/*线程2的入口,线程2的优先级比线程1低,应该线程1优先执行
static void thread2_entry(void *parameter)
{
int i;
while(1)
{
rt_kprintf("try to release block\n");
for(i=0;i<48;i++)
{/*释放所有的分配成功的内存块
if(ptr[i]!= RT_NULL)
{rt_kprintf("release block %d\n",i);
rt_mq_free(ptr[i]);
ptr[i] = RT_NULL;
}
}
rt_thread_delay(10); //休眠10个OS Tick
}
}
int mempool_simple_init()
{
int i;
for(i=0;i<48;i++) ptr[i] = RT_NULL;
/*初始化内存池对象
rt_mp_init(&mp,"mp1",&mempool[0],sizeof(mempool),80);
/*创建线程1*/
tid1=rt_thread_create("t1",
thread1_entry,
RT_NULL,
THREAD_STACK_SIZE,THREAD_PRIORTY,
THREAD_TIMESLICE
);
if(tid1 != RT_NULL)
rt_thread_startup(tid1);
else
tc_stat(TC_STAT_END| TC_STAT_FATLED);
/* 创建线程2 */
tid2 = rt_thread_create("t2",
thread2_entry, /* 线程入口是thread2_entry */
RT_NULL, /* 入口参数是RT_NULL */
THREAD_STACK_SIZE, THREAD_PRIORITY + 1,
THREAD_TIMESLICE);
if (tid2 != RT_NULL)
rt_thread_startup(tid2);
else
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
动态内存管理:
动态内存管理是一个真实的堆(Heap)内存管理模块,可以在当前资源满足的情况
下,根据用户的需求分配任意大小的内存块。而当用户不需要再使用这些内存块时,又可以
释放回堆中供其他应用分配使用。RT-Thread系统为了满足不同的需求,提供了两套不同的
动态内存管理算法,分别是小堆内存管理算法和SLAB内存管理算法。
小堆内存管理模块主要针对系统资源比较少,一般用于小于2M内存空间的系统;而
SLAB内存管理模块则主要是在系统资源比较丰富时,提供了一种近似多内存池管理算法的
快速算法。两种内存管理模块在系统运行时只能选择其中之一或者完全不使用动态堆内存管
理器。这两种管理模块提供的API接口完全相同。
• 警告:因为动态内存管理器要满足多线程情况下的安全分配,会考虑多线程间的互斥
问题,所以请不要在中断服务例程中分配或释放动态内存块。因为它可能会引起当前
上下文被挂起等待。
小内存管理模块:
小内存管理算法是一个简单的内存分配算法。初始时,它是一块大的内存。当需要分配内存块时,
将从这个大的内存中,分割出相匹配的内存块,然后把分割出的空闲内存块还给堆内存管理系统中。
每个内存块中都包含一个管理用的数据头,通过这个头把使用块与空闲块用双链表连接起来。
每个内存块(不管是已分配的内存块还是空闲的内存块)都包含一个数据头,其中包
括:
magic – 变数(或称为幻数),它会被初始化成0x1ea0(即英文单词heap),用于标
记这个内存块是一个内存管理用的内存数据块;
used - 指示出当前内存块是否已经分配。
magic变数不仅仅用于标识这个数据块是一个内存管理用的内存数据块,实质也是一个
内存保护字:如果这个区域被改写,那么也就意味着这块内存块被非法改写(正常情况下只
有内存管理器才会去碰这块内存)。
内存管理的在表现主要体现在内存的分配与释放上,小型内存管理算法可以用以下例子
体现出来。
如小内存管理算法链表结构示意图所示的内存分配情况,空闲链表指针lfree初始指向
32字节的内存块。当用户线程要再分配一个64字节的内存块时,但此lfree指针指向的内存
块只有32字节并不能满足要求,内存管理器会继续寻找下一内存块,当找到再下一块内存
块,128字节时,它满足分配的要求。因为这个内存块比较大,分配器将把此内存块进行拆
分,余下的内存块(52字节)继续留在lfree链表中,如下分配64 字节后的链表结构所示。
另外,在每次分配内存块前,都会留出12字节数据头用于magic,used信息及链表节点
使用。返回给应用的地址实际上是这块内存块12字节以后的地址,前面的12字节数据头是
用户永远不应该碰的部分。(注:12字节数据头长度会与系统对齐差异而有所不同)
释放时则是相反的过程,但分配器会查看前后相邻的内存块是否空闲,如果空闲则合并
成一个大的空闲内存块。
SLAB内存管理模块:
RT-Thread的SLAB分配器实现主要是去掉了其中的对象构造及析构过程,只保留了
纯粹的缓冲型的内存池算法。SLAB分配器会根据对象的类型(主要是大小)分成多个区
(zone),也可以看成每类对象有一个内存池,如SLAB 内存分配器结构所示:
一个zone的大小在32k ~ 128k字节之间,分配器会在堆初始化时根据堆的大小自动调整,
。系统中最多包括72种对象的zone,最大能够分配16k的内存空间,如果超出了16k那么
直接从页分配器中分配。每个zone上分配的内存块大小是固定的,能够分配相同大小内存
块的zone会链接在一个链表中,而72种对象的zone链表则放在一个数组(zone arry)中统
一管理。
下面是动态内存分配器主要的两种操作:
• 内存分配: 假设分配一个32字节的内存,SLAB内存分配器会先按照32字节的值,从
zone array链表表头数组中找到相应的zone链表。如果这个链表是空的,则向页分配
器分配一个新的zone,然后从zone中返回第一个空闲内存块。如果链表非空,则这
个zone链表中的第一个zone节点必然有空闲块存在(否则它就不应该放在这个链表
中),那么就取相应的空闲块。如果分配完成后,zone中所有空闲内存块都使用完
毕,那么分配器需要把这个zone节点从链表中删除。
• 内存释放:分配器需要找到内存块所在的zone节点,然后把内存块链接到zone的空闲
内存块链表中。如果此时zone的空闲链表指示出zone的所有内存块都已经释放,即
zone是完全空闲的,那么当zone链表中全空闲zone达到一定数目后,系统就会把这个
全空闲的zone释放到页面分配器中去。
动态内存接口
初始化系统堆空间
void rt_system_heap_init(void* begin_addr, void* end_addr);
begin_addr 堆内存区域起始地址;
end_addr 堆内存区域结束地址。
分配内存块
从内存堆上分配用户指定大小的内存块,函数接口如下:
void* rt_malloc(rt_size_t nbytes);
重分配内存块
在已分配内存块的基础上重新分配内存块的大小(增加或缩小),可以通过下面的函数
接口完成:
void *rt_realloc(void *rmem, rt_size_t newsize);
参数描述
rmem 指向已分配的内存块;
newsize 重新分配的内存大小。
分配多内存块
从内存堆中分配连续内存地址的多个内存块,可以通过下面的函数接口完成:
void *rt_calloc(rt_size_t count, rt_size_t size);