一、目的
嵌入式RTOS中最重要也是最容易被忽略的一个组件就是内存管理,像FreeRTOS单单内存管理组件就提供了heap_1/2/3/4/5这五种方案,每种方案都有其特点和应用场景。
一般情况下小系统所运行的芯片平台本身内存就很少,有些时候内存空间还不连续(分散在多个地址区间内)。在一些特定场景下为了保证内存分配的确定性,我们会选择使用内存池的分配方案,也就是固定块内存分配方案,对应于RT-Thread中就是MEMPOOL方案。
本篇我们就从源码层面给大家讲讲RT-Thread的MEMPOOL的实现原理。
内存池分配方案的特点:
每个内存块大小固定,故内存利用率可能不高
不存在内存碎片
分配时间相对固定
实现简单
可以通过实现多个不同大小不同属性的MEMPOOL满足特定场景
二、介绍
RT-Thread的每个MEMPOOL都对应于一个struct rt_mempool数据结构,如下
struct rt_mempool
{
struct rt_object parent; /**< inherit from rt_object */
void *start_address; /**< memory pool start */
rt_size_t size; /**< size of memory pool */
rt_size_t block_size; /**< size of memory blocks */
rt_uint8_t *block_list; /**< memory blocks list */
rt_size_t block_total_count; /**< numbers of memory block */
rt_size_t block_free_count; /**< numbers of free memory block */
rt_list_t suspend_thread; /**< threads pended on this resource */
};
各字段含义:
parent:每个mempool对象都是一个struct rt_object类型的对象(基类),通过parent字段可以将mempool统一管理;
start_address:内存池对应的内存首地址(对齐)
size:内存池对应的内存总大小(对齐)
block_size:每个内存块可分配的最大内存大小,注意此大小仅仅是外部看到的内存块大小,实际每个内存块的大小稍大(用于存放块信息)
block_list:指向空闲内存块链表中的第一个空闲块
block_total_count:记录此MEMPOOL总共包含的内存块
block_free_count:记录此MEMPOOL当前空闲的内存块个数
suspend_thread:记录由于内存块不足挂起的线程
下面我们来分析下MEMPOOL的初始化函数:
rt_err_t rt_mp_init(struct rt_mempool *mp,
const char *name,
void *start,
rt_size_t size,
rt_size_t block_size)
{
rt_uint8_t *block_ptr;
register rt_size_t offset;
/* parameter check */
RT_ASSERT(mp != RT_NULL);
RT_ASSERT(name != RT_NULL);
RT_ASSERT(start != RT_NULL);
RT_ASSERT(size > 0 && block_size > 0);
/* initialize object */
rt_object_init(&(mp->parent), RT_Object_Class_MemPool, name);
/* initialize memory pool */
mp->start_address = start;
mp->size = RT_ALIGN_DOWN(size, RT_ALIGN_SIZE); //对齐
/* align the block size */
block_size = RT_ALIGN(block_size, RT_ALIGN_SIZE); //对齐
mp->block_size = block_size;
/* align to align size byte */
mp->block_total_count = mp->size / (mp->block_size + sizeof(rt_uint8_t *)); //注意此处多出的sizeof(rt_uint8_t *),下面会具体分析原因
mp->block_free_count = mp->block_total_count;
/* initialize suspended thread list */
rt_list_init(&(mp->suspend_thread));
// 构建空闲内存块链表
/* initialize free block list */
block_ptr = (rt_uint8_t *)mp->start_address;
for (offset = 0; offset < mp->block_total_count; offset ++)
{
*(rt_uint8_t **)(block_ptr + offset * (block_size + sizeof(rt_uint8_t *))) =
(rt_uint8_t *)(block_ptr + (offset + 1) * (block_size + sizeof(rt_uint8_t *)));
}
*(rt_uint8_t **)(block_ptr + (offset - 1) * (block_size + sizeof(rt_uint8_t *))) =
RT_NULL;
mp->block_list = block_ptr;
return RT_EOK;
}
为了方便讲解我这边整理了一张图
注意 struct rt_mempool_item是为了介绍方便虚拟出来的结构,实际上不存在。
如图,每个内存块A/B/C最开头都有一个uint8_t类型的指针,这个指针的作用就是将空闲内存块连接起来;在分配内存时存放已分配内存块所从属的MEMPOOL指针。
mp->block_total_count = mp->size / (mp->block_size + sizeof(rt_uint8_t *));
此行代码分母部分要多出sizeof(rt_uint8_t *)字节的原因就是为了存放此指针。
block_ptr = (rt_uint8_t *)mp->start_address;
for (offset = 0; offset < mp->block_total_count; offset ++)
{
*(rt_uint8_t **)(block_ptr + offset * (block_size + sizeof(rt_uint8_t *))) =
(rt_uint8_t *)(block_ptr + (offset + 1) * (block_size + sizeof(rt_uint8_t *)));
}
*(rt_uint8_t **)(block_ptr + (offset - 1) * (block_size + sizeof(rt_uint8_t *))) =
RT_NULL;
上面的代码行就是将空闲块通过指针的方式连接起来,最后一个空闲块头部指针设置为NULL。
分析完初始化代码,我们来看一下内存分配接口
void *rt_mp_alloc(rt_mp_t mp, rt_int32_t time)
{
rt_uint8_t *block_ptr;
register rt_base_t level;
struct rt_thread *thread;
rt_uint32_t before_sleep = 0;
/* parameter check */
RT_ASSERT(mp != RT_NULL);
/* get current thread */
thread = rt_thread_self();
/* disable interrupt */
level = rt_hw_interrupt_disable();
while (mp->block_free_count == 0)
{
/* memory block is unavailable. */
if (time == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(level);
rt_set_errno(-RT_ETIMEOUT);
return RT_NULL;
}
RT_DEBUG_NOT_IN_INTERRUPT;
thread->error = RT_EOK;
/* need suspend thread */
rt_thread_suspend(thread);
rt_list_insert_after(&(mp->suspend_thread), &(thread->tlist));
if (time > 0)
{
/* get the start tick of timer */
before_sleep = rt_tick_get();
/* init thread timer and start it */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
/* do a schedule */
rt_schedule();
if (thread->error != RT_EOK)
return RT_NULL;
if (time > 0)
{
time -= rt_tick_get() - before_sleep;
if (time < 0)
time = 0;
}
/* disable interrupt */
level = rt_hw_interrupt_disable();
}
/* memory block is available. decrease the free block counter */
mp->block_free_count--;
/* get block from block list */
block_ptr = mp->block_list;
RT_ASSERT(block_ptr != RT_NULL);
/* Setup the next free node. */
mp->block_list = *(rt_uint8_t **)block_ptr;
/* point to memory pool */
*(rt_uint8_t **)block_ptr = (rt_uint8_t *)mp;
/* enable interrupt */
rt_hw_interrupt_enable(level);
RT_OBJECT_HOOK_CALL(rt_mp_alloc_hook,
(mp, (rt_uint8_t *)(block_ptr + sizeof(rt_uint8_t *))));
return (rt_uint8_t *)(block_ptr + sizeof(rt_uint8_t *)); //返回给调用者的内存地址需要往后偏移sizeof(rt_uint8_t *)字节
}
代码中通过判断mp->block_free_count的值来检查是否有空闲块可用,如果没有空闲块可用就根据time的设置将当前线程进行挂起并设置超时时间;当有空闲块时,通过mp->block_list指针获取到一个空闲块,并且此块头部的指针区域赋值为mp,通过此指针这个分配出来的块就和mempool进行了绑定,这样只要找到内存块就能找到MEMPOOL对象。
下图是分配一个内存块后的内存布局如下
图中内存块A被分配出来后,block_list指向了下一个内存块B
注意此时内存块A的next_ptr指针指向mempool对象
最后我们再分析一下内存回收接口
void rt_mp_free(void *block)
{
rt_uint8_t **block_ptr;
struct rt_mempool *mp;
struct rt_thread *thread;
register rt_base_t level;
/* parameter check */
if (block == RT_NULL) return;
/* get the control block of pool which the block belongs to */
block_ptr = (rt_uint8_t **)((rt_uint8_t *)block - sizeof(rt_uint8_t *)); //block是偏移后的数值,此处需要找到内存块真正的首地址
mp = (struct rt_mempool *)*block_ptr; //已分配内存块的头部存放的是mempool对象指针
RT_OBJECT_HOOK_CALL(rt_mp_free_hook, (mp, block));
/* disable interrupt */
level = rt_hw_interrupt_disable();
/* increase the free block count */
mp->block_free_count ++;
/* link the block into the block list */
*block_ptr = mp->block_list; // 将需要的释放的内存块放到空闲内存块的首部
mp->block_list = (rt_uint8_t *)block_ptr;
if (!rt_list_isempty(&(mp->suspend_thread)))
{
/* get the suspended thread */
thread = rt_list_entry(mp->suspend_thread.next,
struct rt_thread,
tlist);
/* set error */
thread->error = RT_EOK;
/* resume thread */
rt_thread_resume(thread);
/* enable interrupt */
rt_hw_interrupt_enable(level);
/* do a schedule */
rt_schedule();
return;
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
}
/* get the control block of pool which the block belongs to */
block_ptr = (rt_uint8_t **)((rt_uint8_t *)block - sizeof(rt_uint8_t *));
mp = (struct rt_mempool *)*block_ptr;
通过入参block的值找到内存块的首地址,此时内存块的最开始处存放的是当前mempool的指针值,通过这个指针值我们就可以找到当前这个内存块从属于的mempool。
*block_ptr = mp->block_list;
mp->block_list = (rt_uint8_t *)block_ptr;
将当前mempool的block_list的值赋值给当前内存块的开始位置,然后再将mp->block_list设置为当前块的地址值,也就是将当前free的内存块重新挂到mempool空闲链表上(mp->block_list总是存放空闲块链表的第一个内存块,如果为NULL则代表无空闲内存块)。
以上,我们就基本分析完了RT-Thread的mempool代码,总体来看实现上清晰明了。
在具体使用上,我们可以通过创建多个不同粒度的mempool来满足业务需要。