在嵌入式系统设计过程中,许多软件工程师受困于动态内存管理。本文介绍一种将堆栈中的内存碎片降至最少的解决方案,其中讲到了内存碎片和内存丢失的区别,以及一种在编程中有利于检测并消除内存丢失的策略。
标准c库函数malloc()和free()可在任意的时间段中,为应用分配任意大小的内存块。随着内存块的使用和释放,在整个内存区域中,分配给堆栈的存储区将混杂着许多正在使用或已经释放的存储块,而未被使用的任何小块内存区将变得无法使用。例如,某个应用要求堆栈分配30字节,如果堆栈中只有20个长度为3字节的小存储块(总共为60字节),那么堆栈仍然无法为该应用分配内存,因为所需的30字节必须是连续的。
在执行时间较长的程序中,内存碎片可能导致系统的内存枯竭,尽管分配的内存总量并未超出总的可用内存总数。内存碎片的数量取决于堆栈的实现策略。大多数程序员均采用由编译器提供的malloc()和free()函数创建的堆栈,因此内存碎片就不受程序员的控制。
内存丢失是应用程序的缺陷,更具体地,内存丢失是一块已经分配但永远不会被释放的内存区。如果所有指向内存块的指针超出界限或者指向其他的区域,那么应用程序将永远不能释放那块内存区。对于将会在某时刻退出的桌面应用程序,较小的内存丢失还可以承受,因为退出进程将把占用的所有内存返还给操作系统。但对于长时间运行的嵌入式系统,则通常需要确保绝对没有内存丢失。
避免内存丢失不是轻而易举的,为了确保所有分配的内存都在随后释放,必须建立一套明确的规则,以确定哪个应用占用了内存。为跟踪内存,可采用类、指针数组或链表。由于在动态内存分配中,程序员无法预先知道在给定时间内需要分配多少数据块,因此通常需要采用链表结构。
例如,假定一个任务正在接收来自通信信道的消息,任务将为消息分配空间,而该空间在消息得到完整的处理之前不会被释放。因为消息有可能不会按照接收的顺序进行处理,因此一些消息存在的时间将比其他消息更长。所有挂起的消息存在于一个列表中,列表的长度取决于任意给定时间内进行处理的消息数目。嵌入式系统必须将消息转发至另外的设备,而且消息在收到传送确认之前不能被删除。由于消息将传送至许多不同的目的地,而且某些目的地可能存在一些导致重传的故障,因此不能以先入先出方式处理这些消息。
在上述问题中,动态内存管理对ram的利用效率高于预定义缓存管理。当内存不再被消息队列使用时,就能被其他队列或完全不同的程序部分使用。
当多个指针同时指向某个特定的内存块时,通常还会产生另一个特殊问题。如果第一个实体(entity)占有内存并希望释放该内存,那么必须考虑是否还有其他指针指向该区域。如果存在,那么随着第一个实体释放内存,其他的指针将成为悬挂指针(dangling pointer),即该指针指向的空间不再有效。当使用悬挂指针时,或许仍然可以得到正确的数据,但这些内存终将被重新使用(通过另一个malloc()调用),从而导致在悬挂指针和该内存的新使用者之间出现不期望的相互影响。
悬挂指针与内存丢失刚好相反。如果没有释放内存,就可能导致内存丢失;而如果释放了那些并不准备释放的内存则将产生悬挂指针。
内存丢失在许多方面与竞争条件非常相似。内存丢失引发的性能失常完全不同于程序错误,因此,这些问题很难通过调试器对代码进行单步调试加以解决。对于内存丢失和竞争条件,代码检查有时能比采用任何技术解决方案更快地找到问题所在。
添加调试代码并生成输出通常比源代码调试器更为有效,但在某些竞争条件下,则有可能改变代码的执行特性,从而掩盖了问题。在内存丢失中,添加调试代码可改变内存配置,这意味着悬挂指针故障可能具有不同的执行特性。另一缺陷在于,如果调试代码消耗了内存,那么调试版将比产品版更快地耗尽ram,内存丢失就是内存丢失,而不管这些调试代码的副作用如何,都应当可被检测。
驱动自动碎片收集
java具有对无用存储单元进行碎片收集(garbage collection)的自动内存管理机制,因此java程序员无须担心内存分配的释放。如果以前曾用过java进行编程,那么与其他编程语言相比,无疑会对ja