Rt-thread源码剖析3内存管理
Rt-thread源码剖析(3)——内存管理
本文重点
mempool其实存在优先级翻转问题,详见 内存池释放
内存管理概述
首先,内存管理方式可以分为几类,而根据不同的分类标准则可以得到不同的结果。
以 内存获取的时机 为标准,可以分为动态初始化和静态初始化两种方式,这两种内存分配的核心差异在于,静态初始化的内存是在编译是就确定的,他并不涉及内存分配,初始化对应的函数也是 rt_object_init ,而其余所有设计内存分配的方式,都是在程序运行中实现的。
以 动态分配时内存获取的时间是否可控 分类,又可以把动态内存管理分为两类,第一类通过 RT_KERNAL_MALLOC 获取内存,第二类通过 rt_mp_alloc 获取内存
RT_KERNAL_MALLOC :在运行时根据需要分配和释放内存,提供更大的灵活性。动态内存管理又可细分为, 以下三种方式共用同一软件接口,因此只能选其一;而mempool则可与他同时使用:
- 小内存管理(Small Memory Management) :针对小块内存的分配,采用简单快速的算法,但可能产生碎片。
- SLAB 分配 :针对大内存块的分配管理
以上两种方式都是管理一块连续的内存,只是算法不同,而memheap则是多块内存,他们通过宏定义来决定启用哪个
内存堆管理(MemHeap) :系统中存在多个内存堆的时候,用户只需要在系统初始化时将多个所需的 memheap 初始化,并开启 memheap 功能就可以很方便地把多个 memheap(地址可不连续)粘合起来用于系统的 heap 分配
rt_mp_alloc内存池管理(MemPool) :预先分配固定大小的内存块,适用于需要高效、快速分配和释放固定大小内存块的场景,避免碎片化,且获取内存时间是完全固定的,因此又称为 静态内存分配
静态初始化
在 RT-Thread 实时操作系统中,内核对象(如线程、定时器等)可以通过静态方式进行初始化,即在编译时分配内存。这种方式需要用户在调用
rt_timer_init
或
rt_thread_init
等函数时,提供预先分配好的内存块。
静态初始化的特点:
- 编译时确定内存 :所需的内存空间在编译阶段就已确定,不依赖于运行时的内存分配。这意味着在系统运行期间,不会发生内存申请或释放的操作,减少了内存碎片和分配失败的风险。
- 高可控性 :由于内存布局在编译时已知,开发者可以精确控制内存的使用,提高系统的稳定性和可靠性。
- 无运行时开销 :避免了动态内存分配带来的运行时开销,如内存管理算法的执行时间等
该方式其实并不涉及什么内存分配和内存管理,以下着重讲一下设计内存管理的部分
动态初始化
当需要在运行时获取需要的内存时,就涉及到了内存分配
内存分配根据时间是否可控,又分为动态内存分配和静态内存分配(mempool)
Mempool静态内存分配
首先要感性地理解内存池是怎么样的一个东西:
有一块内存,基本上是static修饰的静态一个数组,然后你需要把他切成一块块的block,这个block是别人想要用内存时的单位,不管对方需要多少,每次获取就是给一个block;每个block之间通过链表链接
然后有一个mempool控制块,维护有几个block可用之类的信息
如果想使用内存池,则需要开启RT_USING_MEMPOOL宏
内存池的初始化:
- 静态初始化
:使用
rt_mp_init
函数,开发者需要提供内存池的起始地址、总大小以及每个内存块的大小等信息。 - 动态创建
:使用
rt_mp_create
函数,系统在运行时从堆中分配内存池所需的空间。这种方式虽然在初始化时使用了动态内存分配,但之后的内存块分配和释放操作仍然是静态的。
这两种初始化方式只是作为一个内存对象的创建时存在差异,而且差异仅限于内存池是运行时申请的还是编译时就确认的,其他对内存池的初始化不存在差异,当使用时不存在任何差异
接下来依次看看他们的源码
首先是控制块结构体
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 */
};
我们结合初始化函数来看看这个结构体有什么用,假设通过如下代码对
rt_mp_init
进行调用
static rt_uint8_t mempool[4097];
static struct rt_mempool mp;
rt_mp_init(&mp, "mp1", &mempool[0], sizeof(mempool), 80);
rt_mp_create
和
rt_mp_init
在逻辑上是完全一致的,他们做了以下这些事:
- 先初始化作为一个内核对象的公共属性,即mp->parent
- 将传入的size进行4字节向下对齐,也就是虽然我提供了4097个字节的空间,但只会用4096
- 将block_size向上四字节对齐
- 我传入了80字节作为一个block,但实际上,每个block好需要一个指针的4字节空间,在加上4字节后,计算可用的block数量是多少,这里是4096/(80+4) = 48
- 将当前申请mempool的线程置空
- 将每个block的指针指向下一块可用的空间
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 *));
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;
}
内存池的释放与内核对象的释放一致,根据初始化的不同,分别调用rt_mp_detach和rt_mp_delete即可
内存池的申请
void *rt_mp_alloc(rt_mp_t mp, rt_int32_t time)的核心代码如下所示,首先我们忽略没有内存块可用的情况
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 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 *));
}
核心的逻辑如下所示
block_ptr = mp->block_list;
mp->block_list
是内存池中空闲内存块链表的头指针。block_ptr
指向当前要分配的内存块。
mp->block_list = *(rt_uint8_t **)block_ptr;
- 每个空闲内存块的前几个字节存储了指向下一个空闲块的指针(即链表的下一个节点)。
- 这行代码将
mp->block_list
更新为下一个空闲块的地址,从而从链表中移除当前分配的内存块。
*(rt_uint8_t **)block_ptr = (rt_uint8_t *)mp;
这行代码将当前分配的内存块的前几个字节(原本存储下一个空闲块的指针)修改为指向内存池控制块(
mp
)。为什么这样做?这里很重要,因为释放内存的时候要用到
- 在 RT-Thread 的内存池实现中,每个分配出去的内存块需要记录它所属的内存池,以便在释放时能够正确地将内存块归还到对应的内存池中。
- 通过将内存块的前几个字节指向
mp
,可以在释放时快速找到内存池控制块。
return (rt_uint8_t *)(block_ptr + sizeof(rt_uint8_t *));
- 返回给用户的是内存块中用户可用部分的地址,即跳过前几个字节(存储管理信息的区域)。
- 这样,用户可以直接使用返回的指针,而无需关心内存池的管理细节。
接下来看一下如果申请时无内存可用,代码会做些什么
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();
}
总结来说,当内存池内无内存块可用时,会在指定超时时间内进行等待, 该行为不可在中断中进行
检查内存池是否有空闲内存块 :
- 如果
mp->block_free_count == 0
,表示没有空闲内存块,当前线程需要挂起。
- 如果
挂起线程 :
- 将线程插入到挂起线程队列(
mp->suspend_thread
)。 - 如果设置了超时时间(
time > 0
),启动线程的定时器。
- 将线程插入到挂起线程队列(
调度 :
- 调用
rt_schedule
,切换到其他线程执行。
- 调用
线程唤醒 :
线程可能被以下两种方式唤醒:
- 其他线程释放了内存块,并唤醒了当前线程。
- 线程的定时器超时,强制唤醒线程。
检查唤醒原因 :
- 如果线程是因为超时被唤醒,返回
RT_NULL
。 - 如果线程是因为内存块可用被唤醒,继续尝试分配内存块。
- 如果线程是因为超时被唤醒,返回
内存池释放
内存池的释放则相对简单,概括来说进行了以下操作
- 通过需要释放的内存地址的前的四个字节找到该内存块所属内存池的控制结构体,详见内存申请时的红色字体
- 通过头插法将内存块插回可用内存块列表
- 如果存在suspend_thread等待内存块申请,则 唤醒suspend_thread队列中第一个线程
这里一定要注意,如果你使用的是以下仓库,内存池其实是存在优先级翻转的,因为进入suspend_thread的线程句柄并没有按照优先级排序,而唤醒队列中的第一个
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 *));
mp = (struct rt_mempool *)*block_ptr;
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);
}
动态内存分配
动态内存分配大体分两类,一类是针对单块连续的内存进行管理,一类是可以针对多块地址不连续的内存进行管理
对于单块连续内存进行管理的算法又分为针对小内存的管理算法和针对大内存的管理算法
本文以小内存的管理算法为例,下图是小内存分配下,初始化后的结构示意图
首先我们来看一下内存块头部的结构体
struct rt_small_mem_item
{
rt_ubase_t pool_ptr; /**< small memory object addr */
rt_size_t next; /**< next free item */
rt_size_t prev; /**< prev free item */
rt_uint8_t thread[4]; /**< thread name */
};
控制块结构体
struct rt_small_mem
{
struct rt_memory parent; /**< inherit from rt_memory */
rt_uint8_t *heap_ptr; /**< pointer to the heap */
struct rt_small_mem_item *heap_end;
struct rt_small_mem_item *lfree;
rt_size_t mem_size_aligned; /**< aligned memory size */
};
动态内存分配源码非常多,因此仅给出源码的导读
初始化时 ,首先还是先初始化一个控制结构体,然后会预先初始化两个内存块的头部,分别放在内存的头和尾,在内存块的头部中,会标记该头部至下个头部之间的内存是否已经使用,同时通过一个链表连接每个头部以及控制结构体
在分配时 ,通过控制块结构体的lfree找到第一个可分配的内存块,如果他的大小满足使用;,然后会去判断,当前可用的空间,减掉用户申请的空间,还够不够一个新的内存块使用,如果够,那就会分割这块内存块,增加一个头部;如果lfreee指向的内存块不满足大小需求,则通过内存块头部之间的链表;遍历内存块的头部,找到第一个满足大小的可用内存块
在回收时 ,将自己的标志位置为free,同时会判断该内存块的下一个内存块是否已经被使用,如果是空闲状态,则将两块内存块合并;如果该内存块的地址低于lfree指向的内存块,则将lfree指向自身