RT_Thread——内存管理
文章目录
- 一、为什么要自己实现内存管理
- 二、RT-Thread 的内存管理方法
- 2.1 小内存管理算法
- 2.2 slab 管理算法
- 2.3 memheap 管理算法
- 三、Heap 相关的函数
- 3.1 rt_system_heap_init
- 3.2 rt_malloc/rt_realloc/rt_calloc
- 2.3 rt_free
- 2.4 rt_malloc_sethook/rt_free_sethook
一、为什么要自己实现内存管理
后续的章节涉及这些内核对象:thread(线程)、messagequeue(消息队列)、semaphore(信号量)和rt_event(事件集)等。
为了让RT-Thread 更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放
。
使用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化API 函数的涉及,甚至可以减少内存的使用。
内存的动态管理是 C 程序的知识范畴,并不属于 RT-Thread 的知识范畴,但是它跟 RT-Thread 关系是如此紧密,所以我们先了解它。
在C语言的库函数中,有mallc、free等函数,但是在RT-Thread 中,它们不适用:
- 不适合用在资源紧缺的嵌入式系统中
- 这些函数的实现过于复杂、占据的代码空间太大
- 并非线程安全的(thread-safe)
- 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
- 内存碎片化
- 使用不同的编译器时,需要进行复杂的配置
- 有时候难以调试
注意:我们经常"堆栈"混合着说,其实它们不是同一个东西:
- 堆,heap,就是一块空闲的内存,需要提供管理函数
- malloc:从堆里划出一块空间给程序使用
- free:用完后,再把它标记为"空闲"的,可以再次使用
- 栈,stack,函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中
- 可以从堆中分配一块空间用作栈
二、RT-Thread 的内存管理方法
RT-Thread 中内存管理的接口函数为:rt_malloc 、rt_free,对应于 C 库的 malloc、free。
RT-Thread 为了满足不同的需求,提供三种内存栈管理方法,三种方法都使用一样的函数进行操作。
在系统运行时只能选择其中一种方法,甚至可以不使用内存堆管理机制也可以:代码里不调用rt_malloc 函数。
通常在 Keill 工程目录,可以找到 rtconfig.h
,可以在里面配置这些宏之一,选择某个文件:
- RT_USING_SMALL_MEM:默认使用的是小内存管理算法(SMALL_MEM)
- RT_USING_SLAB:slab 管理方法
- RT_USING_MEMHEAP:memheap 管理方法
/* Memory Management */
##define RT_USING_HEAP /* 使用堆 */
##define RT_USING_SMALL_MEM /* 使用哪种堆管理方法 */
##define RT_USING_MEMPOOL /* 使用内存池: 从堆里分配内存后进行二次管理 */
2.1 小内存管理算法
小内存管理算法是最简单的内存分配算法,需要内存就分割一块:
- 初始时,内存是一块大内存,需要分配内存时,从这块大内存分割一块内存
- 每块内存,都包含一个管理用的数据头,里面有指向下一个内存块的指针,通过链表的形式串起来
- 数据头包含四个成员:magic(一个固定值,作为标志指示内存块是否被非法改写)、used(指示内存是否分配使用)、next(指向下一个块内存)、prev(指向上一块内存)
使用小内存管理算法时,内存分配过程如下图所示:
- 假设已经使用这个方法管理了内存,当前时刻空闲链表指针 lfree 指向大小为32字节的内存块
- 当用户需要分配一个64字节的内存块时,lfree指向的内存块只有32字节不能满足要求,内存管理器会继续寻找下一内存块
- 下一内存块只有60字节,而且已经分配了,因此继续寻找下一内存块
- 下一内存块有128 字节,且没有使用过,满足需求,但这块内存比较大,分配器会将剩下的内存块,继续放在链表中
- 这里内存块原来为128字节,使用64字节,另外数据头占据12字节,就只剩下52字节
内存释放过程如下:
- 释放时,分配器查看前后相邻的内存块是否空闲,如果空闲则合并为一块大内存
2.2 slab 管理算法
小内存管理算法需要频繁的分配、释放内存,可能会影响系统效率。
当系统内存比较大,我们可以提前分配好若干大小的内存块,直接供给线程使用,可以一定程度的提高效率,但会浪费一些内存。
slab 管理方法的核心在于有多个链表,每个链表中存放相同大小的内存块:
- 每个 zone_array 数据项就是一个链表,里面存放有多个 zone,zone 里有多个固定大小的小内存块
- zone_array[0]里存放这些 zone,它们被分为小内存块,这些小内存块大小都是8字节
- zone_array[1]里存放这些 zone,它们被分为小内存块,这些小内存块大小都是16字节
- …
- zone_array[71]里存放这些 zone,它们被分为小内存块,这些小内存块大小都是16K字节
下面以情景分析的方法演示slab管理方法,假设想分配32字节的空间:
- 初始状态:zone_array[3]为空
- 调用rt_malloc(32),发现zone_array[3]为空,于是向页分配器申请一大块内存,这被称为zone
- 从这个zone中,分配得到一个小内存块,它被称为chunk:下图中32字节的小内存被称为chunk
- zone_array[3]指向 zone,并且 rt_malloc(32)返回这个 chunk
- 再次调用rt_malloc(32),发现zone_array[3]非空,于是从这个 zone 中继续分配 32 字节的chunk:
下面再次以情景分析的方法演示slab 管理方法,假设释放上面分配得到的2 个chunk:
(1) 这两个chunk 会被放入zone的z_freechunk 链表:
(2) 这时这个zone 就全部是空闲状态,如果zone_array[3]还有其他zone 可用于分配内存,那么这个全部空闲的zone就会从zone_array[3]中移除,放入zone_free 链表
2.3 memheap 管理算法
在嵌入式系统中,除了芯片内部的内存,可能还会外扩内存。
memheap 管理算法就是针对这种情况,可以将多个地址不连续
的内存堆,组成一个大的内存堆。
RT-Thread 通过 memheap_item 链表
,将多个内存堆进行连接。
当分配内存时,会先尝试从默认的内存堆分配内存,当分配失败时,再去 memheap_item 链表一次尝试其它内存堆。
对于用于的应用程序而言,不用关心内部实现,就当作在一个内存堆上操作。
这三种算法,可以比作吃西瓜:
- 对于小内存管理算法:一个西瓜,自己要吃多少,就去切多少
- 对于slab 管理算法:一个西瓜,已经切好了若干大小,自己要吃多少,就去拿对应大小的
- 对于memheap管理算法:两个西瓜,第一个吃完了,就去吃第二个
三、Heap 相关的函数
3.1 rt_system_heap_init
函数原型:
void rt_system_heap_init(void* begin_addr, void* end_addr);
作用:如果要使用内存堆,必须调用该函数进行初始化堆,参数为堆的开始地址和结束地址
该函数通常在 board.c
或 drv_common.c
等类似的系统初始化里
3.2 rt_malloc/rt_realloc/rt_calloc
函数原型:
void *rt_malloc(rt_size_t nbytes);
void *rt_realloc(void *rmem, rt_size_t newsize);
void *rt_calloc(rt_size_t count, rt_size_t size);
作用:
- rt_malloc:从内存堆中找到
nbytes
大小的内存,返回内存块地址 - rt_realloc:将已分配的内存
rmem
重新分配为newsize
大小。若大小增加,原来的内存块数据不变;若缩小,后面的数据将被截断 - rt_calloc:从内存堆中分配
count
个,大小为size
的内存块,返回首个内存块的地址
2.3 rt_free
函数原型:
void rt_free (void *ptr);
作用:申请的内存使用完后,必须调用该函数进行释放,否则会造成内存泄漏
2.4 rt_malloc_sethook/rt_free_sethook
函数原型:
void rt_malloc_sethook(void (*hook)(void *ptr, rt_size_t size));
void rt_free_sethook(void (*hook)(void *ptr));
作用:
- rt_malloc_sethook:内存分配完成后,将回调本函数,用户可以自己设置钩子函数的内容
- rt_free_sethook:内存释放完成前,将回调本函数,用户可以自己设置钩子函数的内容