张 虎,黄海于
(西南交通大学 信息科学与技术学院,四川 成都 610031)
随着高速列车仿真模拟、物联网应用等领域对计算机计算速度要求的不断提高,单个的计算机已无法满足高计算速度的要求。将一个大的计算任务分解成若干个小的计算任务,并利用分布式系统[1]将各计算任务分散到不同的计算机上,以独立进程的形式进行并行计算是一种比较好的解决方法。而对各独立进程的运行状态的实时监控和管理是实现分布式系统高效运行和管理的基础。但是传统的子进程监控只能对子进程是否正在运行或者退出做出判断,无法判断子进程是否处在挂起状态,也无法及时地获取子进程的退出码。
针对上述需求,本文提出了一种根据子进程的窗句柄来检测当前子进程运行状态的方法,并结合传统的子进程管理监控方法,设计并实现了一种实时的子进程管理监控软件。该软件用于启动和监控分布式系统中任务调度器分配给本机的任务。目前该软件可以检测子进程的三个状态:正常运行、退出、挂起;可以及时的将子进程的退出码[2]反馈给分布式系统的任务调度器,为任务调度器[3]高效利用计算资源提供了一定的依据;并且为开发人员根据进程的异常退出码对程序进行查错提供了方便。
通常Windows系统使用CreateProcess函数来新建一个子进程。其中CreateProcess函数的最后一个参数的类型是LPPROCESS_INFORMATION结构体,成功创建子进程后,子进程的基本信息就存储在该结构体中。在Windows API的定义中,该结构体包含了子进程的进程句柄、进程ID、主线程句柄和主线程ID。创建子进程成功后可以通过子进程的句柄和Windows提供的wait[4]系列函数等待子进程或者子进程组变为signaled状态,从而立刻获知该进程或进程组退出。
传统的子进程监控方法的好处是:如果子进程确实正常退出,则该方法能够及时地获知子进程已经退出, 并且可以通过Windows提供的 API函数GetExitCodeProcess获取子进程的退出码。但是在大量的实际应用过程中,该方法暴露出了其不足之处。例如,创建一个有错的 (如除0错误)MFC程序,然后通过以上方法启动这个有错的进程,再用wait系列函数等待子进程退出。当子进程运行到错误的语句时会弹出一个错误窗口,如图1所示。
这时wait系列函数没有返回,说明子进程还处在nonsignaled状态。用GetExitCodeProcess函数获取该子进程的退出码时,得到的退出码为STILL_ACTIVE,也就是说父进程认为子进程还在正常运行,无法获取子进程的退出码。
如果在分布式系统中出现这种子进程明明已经出错导致无法继续运行,但是其监控系统认为其还在正常运行的情况,会严重影响分布式系统的负载均衡,不利于分布式系统高效的运行和管理。
Windows支持两种类型的应用程序。一种是基于图形用户界面(GUI)的应用程序,另一种是基于控制台用户界面(CUI)的应用程序[5]。
基于控制台的应用程序属于文本操作的应用程序。它通常不能用于创建窗口和处理消息,并且不需要图形用户界面。虽然基于CUI的应用程序包含在屏幕上的窗口中,但是窗口只包含文本。命令外壳程序CMD.exe是典型的基于CUI的应用程序。基于GUI的应用程序有一个图形前端程序,它能创建窗口,拥有菜单,可以通过对话框与用户打交道,并且可以使用所有的标准Windows组件。
这两种类型的应用程序之间的界限是非常模糊的,所以Windows没有提供API用来判断一个程序是基于GUI的还是CUI的。但是可以通过应用程序在运行时加载的动态库[6]来判断应用程序的类型。
user32.dll和 comctl32.dll两个模块[7]是 Windows用户界面相关应用程序接口,包括窗口消息处理,基本用户界面等特性。结合大量的测试得出,基于GUI的应用程序在运行时肯定会加载这两个模块,而单纯的基于CUI的应用程序在运行时是不会加载这两个模块的。因此,可以通过检测代理,新启动的应用程序是否加载了us er32.dll和comctl32.dll这两个模块来区分应用程序类型。
接下来的工作就是如何检测新启动的进程加载了哪些模块。在Windows API中提供了枚举一个进程所加载模块句柄的接口:EnumProcessModules函数。该函数的原型为:
该函数接收一个进程的句柄,输出该进程所加载的所有模块的句柄数组,并且通过lpcbNeeded参数输出所有模块的句柄所占的字节数。因为在启动子进程的时候,肯定能够得到子进程的进程ID和进程句柄,所以通过EnumProcessModules函数可以方便地得到某个特定的进程所加载模块的句柄。但是通过模块的句柄,还无法直观的得到进程加载的模块的名称。这种情况下,可以通过vc提供的另外一个接口:GetModuleFileNameEx函数来获取各模块的名称。GetModuleFileNameEx的函数原型为:
该函数接收一个进程的句柄和模块的句柄,通过lpFilename参数以字符串的形式输出模块的具体名称。进程的句柄在启动进程时就可以获得,模块的句柄就是之前通过EnumProcessModules函数获得模块句柄数组。
计算资源上运行的代理通过这两个API的配合使用,可以准确获得启动的子进程所加载的模块具体的名称,也就能够确定子进程是否加载了user32.dll和comctl32.dll两个模块。这样代理就可以确定子进程是基于GUI的应用程序还是基于CUI的应用程序。在确定了应用程序的类型之后,根据各种类型应用程序的不同特点,采用不同的子进程监控方法对其进行监控。
在Windows系统中不论是GUI应用程序还是CUI应用程序,在程序启动时都会生成一个窗口。不同的是,GUI应用程序是根据自己的程序需求生成窗口,CUI应用程序是系统为其加载的一个文本控制台窗口。系统为每一个窗口生成了唯一的标示,即窗口句柄。而且Windows提供了一个通过窗口句柄来检测应用程序是否处于挂起状态的API函数,该函数的原型为:BOOL IsHungAppWindow(HWND hWnd)。该函数接收一个窗口句柄作为输入参数,并且判断该窗口所属的进程是否处于挂起状态。当进程处于挂起状态时,函数返回TRUE;当进程处于非挂起状态时,函数返回FALSE。只要能获取到进程所对应的窗口句柄,就能够通过定时调用IsHungAppWindow函数判断GUI应用程序是否处于挂起状态。
但是,在子进程创建的过程中,父进程只能获取到该子进程的进程句柄和该进程的主线程句柄,无法获取到子进程所对应的窗口句柄。所以,如何获取子进程所对应的窗口句柄是基于GUI的应用程序监控方法的关键。
获取窗口句柄的方法有很多种,本应用中针对GUI应用程序和CUI应用程序的不同特点采用了不同的方法获取这两种应用程序的窗口句柄。
如果应用程序是一个基于GUI的应用程序,则操作系统在启动的过程中不会为应用程序创建控制台窗口,而只是加载应用程序。当基于GUI的应用程序启动之后,就根据程序自身的需要生成特定的窗口。这样窗口的进程ID即为应用程序的进程ID。
针对GUI应用程序的窗口进程ID即为应用程序进程ID的特点,获取GUI应用程序窗口句柄采用的方法是在创建子进程之后遍历系统中所有窗口,在遍历的过程中根据窗口的句柄来获取窗口所对应的进程的ID;将获取到的窗口进程ID与创建的子进程的ID进行匹配。如果匹配成功则该窗口就是子进程创建的窗口,可以通过该窗口的句柄调用IsHungAppWindow函数来判断该子进程是否处于挂起的状态。
在遍历窗口句柄时,是通过GetTopWindow和GetNextWindow这两个API函数协同工作完成的;而根据窗口句柄来获取窗口所对应的进程ID是通过GetWindowThreadProcessId函数实现的。具体的实现代码如下:
该函数的输入参数为需要获取窗口句柄的进程的ID。如果查找成功则返回进程所对应窗口的窗口句柄;如果不成功则返回NULL。
通常情况下,基于CUI的应用程序不会创建窗口和处理消息,并且不需要图形用户界面。但是Windows系统会为CUI的应用程序自动加载一个文本控制台窗口的外壳程序。CUI应用程序的标准输入输出都是在这个外壳程序中完成的,所以也可以通过判断该控制台窗口是否处于挂起状态来判断CUI应用程序的状态,即通过IsHungAppWindow函数来判断应用程序是否挂起。
但是,Windows对CUI应用程序的这种处理方式使得CUI程序与CUI程序的窗口具有不同的进程ID。这样就不能通过匹配程序进程ID和窗口进程ID的方法来确定某一个窗口是否属于某一个应用程序。
针对CUI应用程序的以上特点,本应用获取CUI应用程序窗口句柄的方法是通过检测窗口的标题来确定该窗口是否属于某一个CUI程序。采用这种方法的原因是基于CUI的窗口标题肯定为该CUI程序的绝对路径,并且在分布式计算的环境下,各个任务的子进程可能同名但是肯定是存在于不同的目录下的。所以通过标题来确定一个窗口是否属于CUI程序在本应用环境下完全可行。
基于CUI子进程的监控方法是在启动子进程之后,根据子进程的绝对路径调用FindWindow系统函数来获取该CUI子进程的窗口句柄,这样就可以通过定时调用IsHungAppWindow函数来检测应用程序的运行状态。
以上介绍了通过窗口句柄对各种子进程运行状态监控的可行性。但是,基于窗口句柄检测子进程运行状态的方法是定时检测子进程的运行状态。所以如果定时检测的时间较长时,则缺乏好的实时性;如果定时检测的时间较短时,则增加了计算资源的负载。本应用在综合考虑了以上问题之后,采用了定时检测子进程的运行状态和开辟新的线程等待子进程退出两种手段相结合的方法来监控子进程的运行状态。这样同时确保了检测子进程运行状态的实时性又弥补了传统子进程检测方法在子进程出现挂起状态时无法检测的问题。
在分布式计算环境下,计算资源上的代理启动子进程的时间不确定,并且可能同时管理多个子进程。所以为了方便管理,在本应用中建立了带头结点的子进程信息链表用于同时管理多个子进程。链表的结构体定义如下:
其中 ProjectID、ConditionID、ModuleID在整个分布式计算系统中确定唯一一个子进程,并且子进程的可执行文件根据这三个值的不同而存放在不同的目录下。SendFlag变量是用于提示调度器该进程是否已挂起,当该值为1时,说明已经提示调度器该进程挂起,不用重复提醒;当该值为0时,说明还未提示调度器该进程已挂起。
代理启动时会创建子进程信息链表头结点,并会创建定时器T,定时遍历子进程信息链表。当子进程信息链表中除头结点外没有其他的结点时,则等待下一次定时的到来;当子进程信息链表中除头结点外还有其他进程的信息结点,则首先关闭定时器T;然后遍历链表各个结点中的窗口句柄信息,根据进程的窗口句柄判断进程是否处于挂起的状态,如果进程处于挂起的状态,则及时将该进程的信息及进程的当前运行状态发送给调度器,如果进程没有处于挂起状态,则继续遍历下一个结点,最后遍历完成之后重新创建定时器T,定时遍历子进程信息链表。经过大量的实验,最终将定时器的定时时间设为3 s,即每3 s检查所有子进程是否处于挂起状态。
当有任务提交给代理时,代理首先启动相应的应用程序,然后创建新的线程等待子进程的退出,最后判断子进程的类型,获取进程的窗口句柄,为新启动的应用程序创建子进程链表节点。其中等待子进程退出线程中所做的工作有:(1)根据新启动进程的进程句柄调用WaitForSingleObject函数,等待子进程的退出;(2)当子进程退出后获取进程的退出码,并存放在相应的子进程信息链表结点中;(3)获取退出码之后,将该子进程的基本信息、子进程的当前状态、子进程的退出码发送给调度器,线程结束。
以上介绍了在Windows系统环境下对基于GUI和基于CUI子进程监控的实现方法。该方法主要是通过定时检测子进程对应的窗口是否挂起以及开辟新的线程等待子进程退出两种手段相结合的方式实现对子进程的监控。虽然通过定时检测窗口是否挂起的方法存在缺乏实时性的问题,但是通过缩短定时时间也可以将时间控制在毫秒级,在绝大多数的分布式计算应用系统中是可以接受的。
该方法解决了传统的子进程检测方法无法检测子进程挂起状态的问题。对子进程运行状态的检测更准确,提高了分布式计算环境下的资源利用率。
[1]葛澎.分布式计算技术概述[J].微电子学与计算机,2012(5):201-204.
[2]DAVE T.Understanding exit codes[J].Linux Journal,2010(197):24-25.
[3]Xie Tao,Qin Xiao.A Security-qriented task scheduler for heterogeneous distributed systems [J].Lecture Notes in Computer Science, 2006(4297):35-46.
[4]STRVENS W R,RAGO S A.UNIX环境高级编程(第2版)[M].尤晋元,张亚英,戚正伟译.北京:人民邮电出版社, 2005:179-182.
[5]RICHTER J,NASARRE C.Windows核心编程 (第 5版)[M].葛子昂,周靖,廖敏译.北京:清华大学出版社,2008:69-72.
[6]高连生,盛柏林.动态链接库在组态软件中的应用[J].工业控制计算机,2010(6):21-22.
[7]周超.Windows和Linux动态链接库研究及应用 [D].上海:华东理工大学,2007.
[8]Microsoft.QIsHungAppWindow function (Windows) [OL].[2012-11-28].http://msdn.microsoft.com/ZH-CN/library/windows/desktop/ms633526(v=vs.85).aspx