当前位置:C++技术网 > 资讯 > [Win32] 如何实现低消耗监视进程

[Win32] 如何实现低消耗监视进程

更新时间:2016-11-30 23:15:13浏览次数:1+次

    最近编写一个 Windows 平台程序,负责监视指定进程,并记录进程的运行和死亡次数,不需要帮助其进程重新运行。
    
    备注: 如上图所示,进程A可能是两个,或是一个,不确定。如果两个进程A的其中一个死亡,算进程A死亡一次 或 两个死亡,算进程A死亡两次。本程序需在无窗口的环境下运行,就是Win32控制台项目,在控制台下,输入1显示运行和死亡次数,输入0退出程序。

    其实开始我觉得是一件挺简单的事情,但是到了优化方面还是出了很多问题。
    首先把我的解决问题的思路说下:

    A-1)我第一时间想到的还是线程,毕竟控制台程序没有窗口句柄来启动时钟,且主线程是用来给客户输入需求而使用的。

    A-2)线程内流程如 A-3 图所示

    A-3)

    

    A-4) 这方法有个弊端,就是占用了线程资源,导致上下文切换。因为那个进程A是个计算引擎来的。

    B-1) 在这之后我又找了控制台使用时钟的方法,timeSetEvent 函数,就是指定时间让系统来执行回调函数。

    B-2)这个方法还算可以吧,毕竟任务管理器里好像也是用时钟来监视进程的。

    本来这已经算是基本解决的了,但我觉的应该还有其他的方法。就比如杀毒软件,它是如何做到最低消耗来进行进程拦截的呢?

    我的猜测是:这可能是修改系统的汇编代码,在进行新进程创建时先运行到杀毒软件的指定函数里,判断是否可以运行,进程销毁时也通知到杀毒软件那里。又或者是驱动,至于驱动这方面的东西,没有过多研究,貌似可以运行非常高级的命令。

    最后总结问题,如何实现低消耗地监视进程呢?

    感谢C++技术网的回答。


C++技术网会员解答:

    首先很感谢您对C++技术网的支持和信任。同时,我也很欣赏您提问的方式,描述非常详细,说出了自己所有的想法和疑问,让我很快了解到你的问题点,知道的和不知道的。

    下面对问题进行分析解答,请参考。

    监视程序要完成的工作就是,对所有进程的运行状态进行统计,您已经基本实现了程序。但是经过仔细的分析,程序还有些瑕疵,可能是您没有考虑到,所以先对程序的问题进行分析补充,助你完善程序,让统计更加准确。

问题1:使用进程ID作为进程标志是不准确的。

    Windows中的进程ID并不是唯一属于一个进程的,而是会反复利用的资源。当然,当一个ID分配一个一个进程后,只要这个进程一直运行着,这个ID当然是唯一属于这个进程的。但是当这个进程关闭后,这个ID就可以分配给其他新进程使用了。当之前被关闭的进程再次启动后,则分配了一个新的ID。所以,对一个进程来讲,其实拥有过多个ID。如果这个进程关闭后,没有新进程启动,而且很快这个进程又启动了,原先拥有的ID可能还会分配给这个进程,这样两次也可能是一样的ID。

    所以说,基于进程ID的统计,设计上就不准确。为什么进程ID不能永久归属于一个进程呢?因为进程来得快去得快,还不一定一直运行,如果ID一直归属于这个进程,如果系统运行时间比较久,最后ID就不够用了。进程这个东西就是这样来无影去无踪的,所以只能来了就分配,去了就回收。进程ID也是一种系统资源。

    进程ID自然也就无法唯一标识一个进程,当你不确定一个进程是否运行的时候,也不能用进程ID去获取进程句柄,来试图操作进程,因为很可能你得到的进程ID并不是期望的进程。

    所以,如果每次以进程ID来统计,在一段时间内,进程死掉有复活,ID会变化,这样会认为是新进程。这样统计的结果误差太大。而且,如果间隔时间稍微长点,中间启动的进程很快又死掉,是无法捕捉到的。假如开始获取的ID对应的进程死掉后,新进程创建后分配到了这个ID,程序会误认为之前的进程没有关闭。实际上产生了一个进程关闭和新进程的创建,统计程序无法感知。

问题2:使用简单的刷新进程列表,然后统计是否存在也是不准确的。

    如果只是定期刷新进程列表,前面介绍了基于ID的设计,会有很多问题。而刷新本身也会有设计上的问题。不管你刷新的多么频繁,进程的启动和停止都可以很快,快到你无法感知。很多进程再不停的启动停止,基于刷新的做法也是不好及时感知的。如果开始捕捉到一批进程,再下一次刷新之前,你无法知道这段时间,哪些进程死了。

    当然,我相信你都是经过深思熟虑的,其实想的也是比较全面的了,只是稍有欠缺,我分析出来,可以帮助您完善您的设计,让你的程序更加完善。


问题的改善方法:

    基于以上的分析,已经给您准备好了一个改进的方案,供您参考。

    在刷新进程列表的基础上,我们增加一些措施。我们使用一个进程来定期刷新进程列表,然后和之前一样存储起来。当然,我们此时不能简单的根据进程ID来唯一识别进程了。很自然想到的就是进程的名称。而且,不管程序启动和停止多少次,进程的名字始终都是固定的。进程名字就是进程识别的唯一根据。进程名字可以解决进程ID统计不准确的问题。

    那么如何提高刷新进程列表准确性呢?当然,刷新也是需要的。我们要解决的主要就是,已经知道的进程,它死掉之后,我们需要立即知道,立即统计。这样实时性最好。我们无法利用进程名称来实现。

    那用什么实现实时统计死掉的进程呢?使用进程句柄。当我们在枚举进程列表的时候,我们可以获取进程的句柄,并将进程句柄和进程名对应起来。然后我们使用一个线程来等待进程的信号量,使用WaitForSingleObject来死等这个进程句柄,一旦进程退出,这个函数就可以继续往后执行,否则当前线程就会被挂起。

    如此一来,进程一旦退出,我们的监视线程立即被唤醒,然后就可以进行统计,并清除统计进程里进程的句柄。因为进程退出后,进程句柄、进程ID都无效了。

    让一个线程死等一个进程,再配合定时刷新,如果要精度更高,可以刷新频率更快点。可以在统计中增加是否在运行标识,这样可以快速便利列表,可以让刷新频率更高点,资源占用也不会太大。


如何实现低消耗地监视进程

改进一:

    基于前面的分析,我们要实现低消耗,可以降低刷新频率,使用等待信号的方式及时觉察进程的死掉。线程在等待时会被挂起,是不消耗CPU资源的,所以是很低消耗的。而一般进程的启动,只要不是一闪而过,定时刷新时可以及时捕捉到,只要捕捉到,就可以准确统计了。

     实际上,我们的分析不仅给你解决了低消耗的问题,更多的解决了统计准确率的问题。当然,改进一需要使用多个线程来跟踪多个进程,一个进程使用一个线程跟踪。进程关闭,这个线程就结束。新进程就重新创建一个线程跟踪。

改进二:

    当然,另外一种降低消耗的,就是借鉴杀毒软件进程拦截,可以用API拦截(API Hook),监视进程创建和停止的API的调用。基本原理就是将API调用hook之后,系统调用这个API的时候,会先执行注册的hook函数,就是一个回调函数。这样只要获取一次进程列表,就可以不断的处理hook函数,来更新统计了。这种方法会比较高级些,技术难度也就高些,一般人没有接触过可能不会。所以,想到最多的就是问题中的方案,再稍微深入点就是改进一的做法。

    终极做法还是改进二这里的,是最准确和最低消耗的。

    可以建议你先使用改进一改进一下,然后后面再使用改进二来改进。这里分析就只提供方案,实现代码请自己查询和使用。

    再次感谢信任和支持C++技术网,我们非常用心的解答了这个问题,希望能够帮到您。