引言
内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏分为两种情况:堆内存泄漏和系统内存泄漏。堆内存是指应用程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显式释放的内存。本文所描述的方法只针对发生在虚拟操作系统里的堆内存泄漏。
在诸如通信设备这样采用实时嵌入式操作系统(RealTime Operating System,RTOS)[1]上开发应用程序,通常会在RTOS上搭建虚拟操作系统。虚拟操作系统处于应用程序和RTOS之间,屏蔽了操作系统细节,提供了诸如内存管理、进程调度、定时器管理、消息分发、有限状态机(Finitetate machine,FSM)[2]等功能,以接口的形式向上层应用程序提供虚拟开发环境。虚拟操作系统相对于实时操作系统的位置可以参考图1。
图1 虚拟操作系统相对于实时操作系统的位置图
有了虚拟操作系统后,内存的申请和释放都在虚拟操作系统里实现。应用程序不直接接触内存,这样内存泄漏只可能发生在虚拟操作系统里。虚拟操作系统有两个重要功能:内存管理和进程管理。
(1) 内存管理
系统上电初始化时,虚拟操作系统,就将所有内存一次性申请好并按大小组织成多个堆栈。为了防止系统运行引起的内存碎片,一般将内存空间划分成多种大小,如16 B、32 B、64 B、128 B、256 B、512 B、1 KB、2 KB、4 KB、8 KB、16 KB,各种大小内存块的数量可根据需要配置。预分配的每块缓冲区为一整段,用selector表示。初始化时,将全部selector值按大小压入各自的堆栈。当应用程序申请分配内存(GET_UB)时,虚拟操作系统从相应堆栈中弹出一个selector值,并返回给应用程序使用;当应用程序释放内存(RET_UB)时,虚拟操作系统将要释放内存的selector值压入堆栈。
(2) 进程管理
虚拟操作系统在嵌入式实时操作系统的基础上虚拟了进程和进程调度的概念,在任务基础上实现了二次调度。进程是一种扩展的有限状态机,基本上处于等待消息状态。当接收到一个消息时,进程作出响应,执行特定的动作。进程有3种调度状态:运行状态、就绪状态、阻塞状态。进程调度的状态转换图如图2所示。
图2 进程调度的状态迁移图
进程被创建时,处于阻塞状态。当收到消息时,进程调度将其放到所属任务的就绪队列中,进入就绪状态,等待CPU资源,一旦得到CPU,就进入运行状态。当进程无消息需要处理时,虚拟操作系统会挂起该进程,放到所属任务的阻塞队列中,并设置为阻塞状态。一个进程始终在这3种状态之间转换。
进程调度是在实时多任务操作系统的任务调度基础上实现的。进程本身无显式的优先级表示,但当与每个任务联系起来时,就赋予了与任务等同的优先级,因而从上层应用来看,进程安排在不同优先级的任务中,就具有了不同的优先级。进程可以用一个进程控制块(Process Control Block,PCB)的结构来标识,PCB保存了进程的所有运行状态信息,如当前状态、当前事件等。
1 定位虚拟操作系统内存泄漏的方法
传统检测内存泄漏的方法是截获对分配内存和释放内存函数的调用。截获住这两个函数,就能跟踪每一块内存的生命周期。比如,每当成功分配一块内存后,就把它的指针加入一个全局的链表中;每当释放一块内存后,再把它的指针从链表中删除。这样,当程序结束的时候,链表中剩余的指针就指向那些没有被释放的内存,这是定位内存泄漏的一般方法。按照这种定位方法,可以在虚拟操作系统分配和释放内存的地方对每一块分配的内存进行跟踪,但是只跟踪泄漏的内存是不够的,因为内存的内容是应用程序写入的,没有统一的结构,即使跟踪到了也很难进行分析。
在虚拟操作系统中,要有效定位内存泄漏并对内存泄漏的源头进行分析,就需要对泄漏现场和泄漏内存同步进行记录,也就是对泄漏点的进程调度情况和泄漏内存的内容同时进行记录。这样,当发生内存泄漏后,根据记录的泄漏现场,可以定位发生泄漏的进程和泄漏时的进程运行状态。如果这不足以定位泄漏的原因,可以继续分析泄漏内存中的内容来查明逻辑上的原因。因此,虚拟操作系统中定位内存泄漏的关键在于对泄漏现场(也就是对泄漏点虚拟操作系统进程调度现场)的记录。
本文的核心思想是:先构建一个链表,当发生内存泄漏后,将泄漏点的系统状态信息和泄漏的内存保存到链表中,并在系统崩溃前将链表存储到存储介质(如硬盘)中,事后对记录的文件进行分析,通过对泄漏点的系统状态以及泄漏内存的内容进行还原,来准确定位内存泄漏点。
2 定位虚拟操作系统内存泄漏的实现
通过下述步骤实现内存泄漏定位程序后,在虚拟操作系统中对内存进行分配和归还的地方插入该定位程序。
① 定义发生内存泄漏的标准,包括判断内存发生泄漏的阈值以及记录文件到硬盘的阈值,比如可用内存少于30%时认为发生了内存泄漏,当可用内存少于10%时开始保存链表到硬盘中。这样,在系统正常情况下,本文描述的方法并不启动,不会影响对实时性要求非常高的设备(诸如通信设备)的正常运行。
② 定义一个结构UBLEAK_REG,用来保存泄漏点的系统运行状态。该结构至少包括以下信息,主要是在泄漏点被调度进程的PCB的内容:调用分配内存函数(GET_UB)的行号、
调用释放内存函数(RET_UB)的文件名、内存块(UB)的地址、当前任务号、当前进程名、当前进程在进程属性表中的索引、当前进程的进程号、当前进程当前状态、当前进程上一个状态、当前进程当前事件、当前进程上一个事件、当前事件发送进程的进程号、当前进程的堆栈内容、当前申请UB的时间。
③ 针对每种大小的内存,分别定义一个链表UBLEAK_STACK,用来存储结构UBLEAK_REG的内容。初始化时应依据各内存块的总数和判断泄漏的标准,来预分配链表的大小。对链表存储空间的分配应该有个算法,使得不同大小内存占用的链表空间是可配置的,最简单的情况就是平均分配给不同大小的内存块。
④ 每次应用程序申请内存时,依据步骤①定义的标准判断是否发生了内存泄漏,一旦认定发生了泄漏,将当前进程运行状态信息填写到一个UBLEAK_REG结构中,并将这个结构插入该内存对应的UBLEAK_STACK链表。记录内存泄漏情况的链表结构图如图3所示。
图3 记录内存泄漏情况的链表结构图
⑤ 应用程序释放内存时,同样要判断是否发生了内存泄漏,若未发生则不做任何处理,若已发生则需要从链表中找到该内存的相关信息记录并删除。这样可以在系统发生内存泄漏时,将应用程序正常的内存申请和释放信息排除,不占用宝贵的UBLEAK_STACK资源。
⑥ 如果内存泄漏达到系统崩溃边缘(阈值可定义),则需要保存链表到存储介质(如硬盘)中。在将链表保存到硬盘的过程中应该注意的是,保存的不只是泄漏点的系统状态,还有内存块的内容。保存动作如下:
将泄漏点现场信息写到硬盘中;
根据泄漏内存的地址指针,保存对应的内容到硬盘中;
继续记录下一个泄漏点信息。
⑦ 在内存泄漏导致系统崩溃后,可以从存储介质中将记录内存泄漏信息的文件拷贝出来进行分析。这时,首先需要编写解析该文件的工具,因为存储的文件是二进制格式,需要转换成文本格式以便于阅读,当然每条记录的结构是已知的。接着对照文本文件的内容和UBLEAK_REG的结构,可复原泄漏点的进程状态:对照UBLEAK_REG结构,根据文件名和行号可知进程哪一行发生泄漏;根据进程当前状态和当前事件,可知进程泄漏时的状态和导致泄漏的事件;根据当前事件发送进程号,可知是哪个进程在发消息并最终导致了泄漏;根据进程堆栈内容,对照收到消息的结构,可对当前消息的内容进行详细分析,一般到这里泄漏点已经准确定位了。如果准确定位了内存泄漏点后,还不能定位内存泄漏的根本原因,则可对泄漏内存的内容进一步分析。分析方法是:在内存泄漏点的代码中找到填入内存的数据结构,对照内存的内容,逐个字节进行比较以还原收到的消息内容。根据收到的消息内容和泄漏点处理代码,可读取该消息的处理过程,进一步查找泄漏原因。
结语
本文描述了一种定位虚拟操作系统内存泄漏的方法,通过详细记录实时系统发生内存泄漏时内存申请的系统运行状态,并在系统崩溃前将信息保存到诸如硬盘的媒体介质中,可以有效定位内存泄漏的原因。