这一篇,我们将深入讨论内存管理,所谓的内存管理其实就是动态内存的管理,只有动态内存才会有生命周期,又开始有结束,他才会有需要管理的问题,例如我们人类,假如是静态的内存,从被定义开始,他就是永恒存在的,不需要管理。
在C语言中,经常使用malloc()和free()函数来动态的申请和释放内存,这种方式是基于堆来管理内存的方式,这种方式在嵌入式实时系统中,会存在很多问题隐患:
1.频繁的使用这种方式进行内存的分配和释放,会把堆搞得支离破碎,并因为不能再分配更多的内存而导致应用程序崩溃。
2.基于堆得内存管理是浪费的,所有的堆管理算法必须为每个被分配的块维护某些头部信息,造成内存额外的开销。
3.malloc()和free()函数在实际申请和释放内存的时候,执行的时间是不可确定的,这意味着他们潜在的可能需要一段长的时间,这与我们的实时原则是矛盾的。
然而,堆的问题并不仅仅限于以上这些,在多线程环境中使用堆时,会引入新的问题,首先堆变成了一个共享资源,这引入了并发的问题:
1. malloc()和free()函数是不可重入的,也就是说,他们不能从多个线程被安全的调用。当然可以通过互斥体来弥补API无法重入的问题,但是涉及到互斥体,你又该考虑线程阻塞的问题以及由于互斥体的加入,是否会造成优先级反转的问题,问题似乎变得更复杂了。
2.malloc()和free()是必须成双入对,当你需要使用时,申请了一块内存,当你使用完该内存时,一定要主动地去释放他,不然它会一直占用该内存,变成了静态内存的存在,相当于在动态内存里面开了一块静态的内存,当你只开一块或者几块的时候,也没问题,怕就怕,某种情况下触发了频繁这样的去申请内存,而在使用完时,又没有释放,积少成多,你的堆就被消耗干净了。
3.与第二种方式相反,当你申请了一块内存以后,在应用程序还没有使用完的时候,被意外释放了,那么将会造成dangling指针,当应用程序再次使用该指针时你的应用程序可能崩溃。
4.堆相关的问题是出名的难以测试。
讲了这么多堆的缺点,堆有个最大的优点,可以申请可变大小的内存块,这是我们接下里介绍的内存池的方式无法提供的,凡是总是有利有弊,引入内存池是为了解决以上由于使用堆而带来的那些问题。
为了更简单,更高性能和更安全的使用动态内存,一个通用的方案是,定义一块尺寸固定的堆,也被称为内存池,对于QF框架来说,在管理动态事件时,使用内存池可能是一个更好的选择。
先来看看使用内存池的缺点,首先他只能以一种固定尺寸的块,但是事件的尺寸并不是统一的,所以为了支持所有的事件,块的尺寸必须被定义为能够支持的最大尺寸的事件,这样会造成内存的浪费,折中的方案是定义多个不同尺寸的块的内存池,用于支持不同尺寸的事件,QF最大支持三种不同尺寸的内存池,你可以定义小中大三种。
接下里说说他的优点:
1.内尺寸不会因为频繁的申请和释放变得碎片化。
2.申请的速度远快于堆申请速度,仅需临界区操作,避免的阻塞的情况。
3.动态的事件管理完全由QF框架掌控,他自身就有垃圾回收机制,类似java,所以只管申请,不用担心释放问题。
事件的管理完全交给了QF框架,那么QF需要知道以下几件事:
1.这个事件需不需要我管理(动态还是静态事件)。
2.这个事件来自于哪个内存池。
3.这个事件在什么情况下被释放(释放条件)。 以上这些内容在事件被定义时就被确认了,也就是由事件本身来维护,如图:
关于内存池的使用有两种情况,一种是当QF框架和其它RTOS一起使用时,他可以借用RTOS现有的消息队列,这种情况后面讲到QP+RTOS方案时,我们具体再聊,这里主要介绍一种原生的QF事件队列,所谓原生就是由QF给出的解决方案。 首先我们来看一下QF是如何管理事件池的,其控制方案由一个控制块+内存池的方案组成:
接下来我们看一下如何实际定义一个内存池及如何从内存池中申请内存:
1.先定义内存池。
2.初始化内存池。
3.为事件申请内存
动态事件申请内存不需要释放,QF框架会根据实际情况来回收事件内存。