嵌入式工程狮的升级打怪之路

[手搓RT-Thread]11、※内存管理

内存管理是最基础的,最根本的,单片机无非不是跟内存、内核打交道,任何程序都离不开内存,但是单片机的内存并不像台式机那样大。stm32f103zet6的RAM空间为64K,相较于大多数单片机而言已经很充足了,但是如何分配好这些空间也是一门学问。

1、RTT内存使用注意事项

在之前的章节中,我们留下了一个疑难杂症,参见:[手搓RT-Thread]8、消息队列、信号量及堆栈分析 – www.hawkjgogogo.com的最后一部分,截图如下。

在最后我们对board.c文件中的RT_HEAP_SIZE进行了修改:

划重点:我们需要知道这种开辟一个很大的堆空间进行管理,是通过数组来分配的,由于数组的元素类型为uint32_t,所以每增加一个元素会开辟4个字节空间,但是我们平时只会考虑占用了多少个单字节空间——在设置RT_HEAP_SIZE时需要将我们像设置的空间大小除以4

2、单片机中的内存结构

3、静态内存分配

静态内存一旦创建就指定了内存块的大小,只能根据内存块的大小粒度进行分配。适合于确定需要占多少内存的情况,分配的效率更高,但是利用效率更低。

在RTT中使用静态内存管理时主要利用了内存池(Memory Pool)的概念,也就是在创建时申请了一大块内存,然后分成大小相等的多个小内存块,小内存块通过链表相连接。

每次需要分配内存时便从空闲内存链表中取出表头上的第一个内存块,提供给申请者。

物理内存中允许存在多个大小不同的内存池,并且内存池一旦初始化完成就不能够继续调整。内核负责分配内存池对象控制块,并接收用户线程的分配内存块申请。

静态内存控制块

与其他对象一样,静态内存(内存池)也需要一个静态内存控制块来管理,结构如下:

首先我们需要像分地皮一样划分一片区域给内存池,接着才能像分房子一样将内存池内的内存块挨个分给内存块。

首先我们需要创建一个内存池:

接着我们需要给这个内存池分配对象并设置结构体参数,注意分配内存的过程,向上对齐,加上指向内存池控制块的指针大小,最后再乘上数量:

然后便是通过rt_malloc()函数进行内存开辟,但是在这之前我们还需要知道最开始初始化的时候堆空间的开辟。

堆空间初始化rt_system_heap_init(void *begin_addr, void *end_addr)

根据数组分配地址并对齐:

初始化两个堆控制块,一个在开头一个在结尾。
注意控制块的prev和next,存储的应该是偏置的值。

可以看到,由于Heap_ptr是指向8位变量的,所以可以看作也是一个大型数组(一个超大的旅馆),里面有4084个小房子,每个小房子里面就是一个字节!

最后,将空闲内存块指针等于初始位置的内存控制块:

在本章节我们需要留意的是lfree、heap_ptr、heap_end,在接下来的rt_malloc()中会使用到。

回到rt_malloc()

我们已经初始化的堆内存空间是这样子的:

我们需要像插入链表节点一样,在freeMEM中开辟新的内存空间:

当然在遍历的过程中,我们需要判断该内存块是否已被使用,以及它的大小至少需要满足能够包含MIN_SIZE_ALIGNED 和 struct heap_mem内存控制块。

如果满足条件的话我们就开辟内存空间,设置下一个内存控制块:

接着如果此时的当前内存控制块==最低空闲块儿,则我们需要把最低的空闲块儿指针根据链表连接的节点往后移动,一般来说如果逐个开辟的话就是会往后移动一个到新开辟的代码块儿。

最后将以上地址返回,示意图:

上图中的malloc就是开辟的空间!

静态内存分配

静态内存分配也使用到了rt_malloc()函数,其主要是就将程序启动时初始化的堆空间再分配。先确认需要分配空间的大小,如果设置每个内存块大小为3,先向上对齐到4,再给每个内存块分配一个内存池控制块指针,总计8Byte,最后乘上内存块的数量20,这样就开辟了160Byte的空间(私以为如果内存块的空间较小,性价比不高,因为每个内存块都要加上4Byte的指针)。

重点在后面的内存块的逐个开辟,使用keil自带的memory窗口可以清晰地看出内存的变化:

第一次循环:

循环结束:

这样就开辟了20个内存块,每个内存块分配了3Byte的空间,但是由于字节对齐和链表节点的存在,每个内存块占8Byte的空间,总计160Byte。

注意该函数中的*(rt_uint8_t **)使用到了二级指针,但本质上可以化简为
rt_uint8_t *,即指向一个字节的指针。

也可以理解为(block_ptr + offset * (block_size + sizeof(rt_uint8_t *)))是一个保存了一个指针的地址,而再加上一个*便解引用了,得到了这个指针,跟我们上面所讲到的相符合。

最后返回memorypool指针

静态内存开辟函数 rt_mp_alloc()

在函数中最重要的是以下部分:

将当前的内存链表节点设置为下一个空闲内存块:

改变当前块的内容,将其标记为已分配,并将其链接到相应的内存池:

下面我们做一个尝试,在第一次alloc之后再次alloc,我们会发现这行代码的作用

/* 改变当前块的内容,将其标记为已分配,并将其链接到相应的内存池 */
*(rt_uint8_t **)block_ptr = (rt_uint8_t *)mp;

即标记该块内存已经被使用!

分配了一个内存块但是还没有写入数据。

写入数据一个4字节的数据DEADBEAF。

写入两个数据:

静态内存释放函数 RT_MP_FREE()

既然有开辟,必然紧跟着释放函数。
释放函数传入的是需要释放的内存块的地址,然后通过指针的偏移获取当前内存池控制块的指针。

将本来的空闲内存块变成当前的free掉的内存块的下一个空闲,可以理解为新空出来的内存在下一次需要分配的时候优先被分配,然后如果还需要分配就会通过这个链表连接到下一个需要分配的空闲链表,无论如何都是连起来的,当前的可分配的空闲链表保存在mp->block_list里面。

这也就和上面的alloc相呼应了,如果我们一直alloc不free,那么这个酒店房间就是连续地开辟的比如101、102、103、104、105、106,下一间等待被开辟的是107,但此时103(block_ptr)退房了,变成空闲,那么103客人通过指针偏移找到酒店前台(mp),酒店前台(mp)将103房设置为当前空闲(mp->block_list),即如果有客人来优先分配103房,并将107房设置为103房分配过后的下一间房间!

注意:虽然103房被设置为空闲了,但是本酒店没有打扫阿姨,DEADBEADF依然存储在原来的数据位置!(然并卵,不知道有啥影响)

静态分配的内容到此结束。

动态内存分配

动态内存分配时的流程是:

  • 初始化内存堆空间:rt_system_heap_init()
  • 申请任意大小的动态内存:rt_malloc()
  • 释放动态内存rt_free()

注意:静态内存和动态内存是可以同时使用的,他们都是在内存堆空间中通过rt_malloc()函数开辟的,只不过静态内存是在这么大的堆空间中承包了一个酒店,然后把分配房间的任务交给了酒店前台,而动态内存就是一个个旅行团,他们一来就是承包整个酒店,一走就收拾干净,不留一点痕迹。

所以说,动态内存分配和静态内存分配在本质上并无差别。

参考小标题rt_malloc()。

rt_FREE()

首先我们开辟了100*4Byte的空间给数组p_dyn

接着向该空间内写入数据:

末尾是89C,初始是70C,转化为十进制正好是400Byte,完美!

接着我们直接进入free函数:

同样的,进行时间回溯,指针偏移到前面的内存控制块:

在这些操作时并不会对内存里的内容进行清空,只会改变它的使用情况,因为之后如果再次被使用数据就会覆盖。

接下来,为了更加高效的进行内存使用,使用plug_hole()函数拼接支离破碎的内存。

内存拼接函数plug_hole()

“Plug hole” 的中文翻译可以是 “填补空洞” 或 “堵塞孔隙”。

可以看到这俩内存块是连在一起的,但是还没有被拼接上:

这样,下一个内存块就完成了他的任务,将它的遗产继承给了当前内存块,他俩就合并了。

向前合并也是同样的道理。当然上一块内存块已被使用,所以就无法合拼咯!

4、完结

以前觉得内存是一个深不可测的玩意儿,蹲在电脑前这些天终于整明白了,
完结撒花!

又向嵌入式的深处迈进了一步!


已发布

分类

来自

标签:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注