让我生不如死的Windows CE内存泄漏
发布日期:2011-05-17
很多实时嵌入式设备是长时间不间断运行的,即使是少许的内存泄漏,也会积少成多,对嵌入式系统带来灾难性的影响。这几天,我在嵌入式软件项目中就饱尝到这个痛苦,让我明白到嵌入式实时系统的应用软件也会有许多内存问题,从而导致嵌入式系统的崩溃。例如非法的内存访问、各种死锁以及诸如堆栈溢出、数组越界和内存泄漏等。
Windows CE作为最流行的一种嵌入式操作系统,现正广泛被应用。我所负责的嵌入式应用程序也是在Windows CE平台上开发的。在进入测试阶段中,我发现有一个程序模块系统内存和CPU资源消耗急剧增加,持续增长到出现OutOfMemoryError为止,然后自动重启。这个问题折腾到我生不如死,痛苦不堪。花了我好几个通宵达旦的加班后,经过分析终于确认Windows CE内存泄漏是造成这次Windows CE系统崩溃的主要原因。这里与大家分享我在开发过程中遇到的内存泄漏的检测和处理解决过程。
一.Windows CE如何进行内存分配?
为了判断是否有内存泄露,我们首先需要了解Windows CE是如何管理内存的。许多嵌入式程序员都有一个共识,就是如果评选在Windows CE 程序中遇到最多的问题,那其中一个问题一定有内存问题。
(1)什么是Windows CE内存管理
一般来说,运行Windows CE的嵌入式设备出于紧凑型的考虑内存都不大,以至于有时候有些程序员会为了节省内存开支而牺牲程序的某些性能。但尽管WinCE系统的内存很小,用来管理内存的函数却十分完善。Windows CE实现了Windows XP中几乎全部的Win32内存管理API。例如,Windows CE支持虚拟内存分配,本地和分离的堆管理,甚至还有内存映射文件。像Windows XP一样,Windows CE支持带有应用程序间内存保护功能的32位地址空间,这一点对于多程序和多线程运行时是非常重要的功能。但是Windows CE毕竟是被设计来应用于实时场合的,所以它底层的内存结构又不同于Windows XP。
Windows CE内核可以在Flash上直接运行,也可以加载到内存中运行。Flash的运行方式,是把内核的可执行映像烧写到Flash上,系统启动时从Flash的某个地址开始执行。在这种情况下,Windows CE系统就像直接读硬盘,存储在Flash上的程序能够以现场执行的方式运行。这种能力对小型系统来说使之在具有巨大的优势,这样这能快速启动一个应用程序,因此这种方法被很多嵌入式系统所采用。另一种是内核加载方式,是把内核的压缩文件存放在Flash上,系统启动时读取压缩文件在内存里解压,然后开始执行。
(2)虚拟内存和函数应用
和大多数现代操作系统一样,Windows CE实现按需调页的虚拟内存机制。由于Windows CE系统使用了虚拟内存,这就给应用程序造成了一个假象,以为计算机安装的内存远远超过自己所需要的数量。Windows CE是32位的操作系统,因此支持4GB的虚拟地址空间。Windows把这些地址空间分给进程和系统使用,每个部分可以获得2GB的虚拟内存。
虚拟内存是内存类型中最基础的。Windows CE 实现了系统的虚拟内存管理,在一个虚拟内存系统中,应用程序主要处理这个虚拟的地址空间,并不涉及到由硬件管理的物理内存。系统调用虚拟内存API来为其它类型内存分配内存,包括堆和栈。Windows CE虚拟内存页可以处在三种状态:自由(free),保留(reserved),或被提交(committed)。
简单说,就是当一个应用程序要查询系统的内存时,可使用虚拟内存API,包括VirtualAlloc,VirtualFree和VirtualReSize函数,这些函数可以直接操作虚拟内存空间的虚拟内存页面。例如,页面可以保留,提交给物理内存,或使用这些函数释放。Windows CE实现了Win32的GetSystemInfo和GlobalMemoryStatus函数。另一个检测系统状态的函数是:void GlobalMemoryStatus(LPMEMORYSTATUS lpmst),通过GlobalMemoryStatus返回的信息可以验证Windows CE内存结构。
(3)释放虚拟内存
不同于Windows XP,Windows CE只支持在堆中分配固定(fixed)的块。这简化了内存块在堆中的处理,但是这使得堆在分配和释放一段时间后会产生碎片。当堆里已经清空的时候,仍然会占用大量的虚拟内存页,因为系统不能在堆中内存页没有完全释放的时候回收这些页。这时,一般情况下是可以通过调用VirtualFree来取消提交,或释放虚拟内存。从物理RAM页中取消提交或者取消映射,但是保持页被保留的状态,当在区域中的所有的页通过VirtualFree被释放时,也应该处在同样的情况下。更确切地说,区域中的全部页要被释放,那这些页要么都是被提交的页,要么都是被保留的页。如果有些页被提交,有些页被保留,那么VirtualFree函数调用就会失败。
实际上,Windows CE会监视系统自由的内存,并对越来越少的内存作出响应。当很少内存可用时,Windows CE首先发送WM_HIBERNATE消息,接下来会限制可能的内存分配。当应用程序被发送了一个WM_HIBERNATE消息后,系统将检测内存级别,确认是否可用内存在限度之上,如果可用内存不足,WM_HIBERNATE消息将被发送给下一个程序,这会持续到所有程序被发送了WM_HIBERNATE消息。
二. 什么是Windows CE内存泄露
虽然Windows CE有许多方法来管理系统内存的运行,但还是有可能发生内存错误的。Win32编程中常见内存错误:①内存分配错误;②使用未初始化的内存;③内存泄露;④使用已经释放的内存资源。
(1)什么是内存泄漏
内存泄漏是指程序在运行过程中申请的内存,在程序结束时没有被释放。我们常说的内存泄漏是指堆内存的泄漏,堆内存是指程序从堆中分配的。一般来说,应用程序是使用从堆中分配到一块内存,使用完后程序必须负责相应的释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
一般来说,在所有时刻Windows CE内存管理器都知道进程所拥有的物理内存和虚拟内存。然而,如果进程分配内存时但由于Bug而无法释放内存(内存泄漏),内存管理器就可能无法了解这些已分配的内存,也无法重新访问这些内存,而必须等到进程退出时回收内存。但需要特别注意的是,同样的程序在Window XP平台上可能没有什么问题,但在缺乏内存的Windows CE平台,经过长时间运行该程序可能会内存耗尽而导致系统重启,这是我在经过几个生不如死的通宵达旦测试后得到的宝贵经验和教训。
因此,内存泄漏引发的性能失常完全不同于程序错误,这些问题很难通过调试器对代码进行单步调试加以解决。对于将会在某时刻退出的桌面应用程序,较小的内存泄漏是可以承受的,因为退出进程将把占用的所有内存返还给操作系统。但对于长时间运行的嵌入式系统,则通常需要确保绝对没有内存泄漏。
(2)常见的内存泄漏原因
常见的内存泄漏有这几种原因:①Windows CE内存碎片。②在局部堆申请的堆只增加不会马上减少,直到程序退出。③程序运行时分配物理内存,当程序使用完后,这些物理内存仍然被占用,直到系统内存不足时分页内存交换到分页文件中,然后才释放掉其占用的物理内存。④Windows CE内存管理的缺陷。
总而言之,内存泄漏产生的主要原因是保留了却不再使用的内存空间。Windows CE虽然有自动管理内存的功能,但内存泄漏也是不容忽视,它往往是破坏嵌入式系统稳定性的重要因素。
三. 如何检测和处理内存泄漏?
如何查找引起内存泄漏的原因,一般有两个步骤:第一是安排有经验的编程人员对代码进行走查和分析,找出内存泄漏发生的位置。第二是使用专门的内存泄漏测试工具进行测试。
(1)代码走读检测内存泄漏
通常在怀疑发生内存泄漏之后,第一步是要弄清楚泄漏了什么数据和引起了什么泄漏。一般说来,一个正常的系统在其运行稳定后其内存的占用量是基本稳定的,不应该是无限制的增长的。根据这样的基本假设,我们持续地观察系统运行时使用的内存的大小,如果内存的大小持续地增长,则说明系统存在内存泄漏。
内存泄漏可通过代码走读来发现和定位,也可以用专用的工具来测试和定位。实际上,对于内存泄漏,代码检查有时能比采用任何技术解决方案更快地找到问题所在。预防内存泄漏的唯一方法就是要求程序员在程序结束时,必须将每个申请的内存都释放。
(2)使用工具检测内存泄漏
一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。在这个时候,我们通常需要使用一些开销较低的工具来监控和查找内存泄漏。查找内存泄漏的工具很多,最常用的释放工具就是dmalloc和mpatrol,这些工具提供了记录并检查所有内存分配的调试版堆栈,从而有利于分析内存泄漏和悬挂指针。
检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用,当截获住这两个函数,我们就能跟踪每一块内存的生命周期。比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。哪么,最简单的内存泄漏检测方式就是截获住这些指针。
总的来说,无论那种方式,我们都需要认真检查应用程序任何内存分配调用的返回代码,因为在Windows CE中比在桌面版本的Windows中有更多的机会导致内存分配失败,从而会导致内存泄漏。
本文为Windows Embedded征文比赛获奖文章。