我们监控平台有台报警服务器,其主要功能是接收前端,TDDC,网管服务器等发送的报警,并依据报警联动配置进行相应的联动操作,最近发现在该服务器运行过程中,通过任务管理器查看其句柄数量会不断增加,以至于影响其他服务器工作,初步怀疑是句柄泄露问题,现对其进行分析排查。
句柄是Windows用来标识应用程序所建立或使用的对象的唯一整数,Windows的内核对象包括进线程,窗口,位图,GDI对象等等。应用程序通过句柄访问内核对象,当使用完内核对象之后需要释放资源关闭该内核对象句柄,如果未能正确关闭,则会造成句柄泄露。
一般而言,如果怀疑发生了句柄泄露,最首要任务是查找泄露的句柄类型,这样有助于后面的排查分析,缩小目标范围。这时可以通过一些辅助工具来帮助分析,如Process Explorer,PCHunter等,这些工具能够非常明了的看到进程所正在使用的内核对象,可以帮助我们找到问题所在。
打开Process Explorer,找到运行的报警服务器进程,该工具的会显示出报警服务器当前创建的内核对象句柄,如图,可以看出该进程的句柄数已经有上千个,而且还在快速增长中。
该工具很方便的一点是,当有新的内核对象创建时,在下方的列表框中会以绿色标识出来,方便查看,观察一段时间发现,不停的有线程对象创建,而且不会关闭,初步猜测应该是线程对象的句柄没有关闭导致。接下来的工作就是要找出这个线程内核对象在哪儿创建的。
查找句柄的创建位置的可以通过windbg来获取,windbg是windows下一款非常强大的内核调试器。使用Windbg的!htrace命令可以调试句柄泄露。其原理比较简单,就是分别为进程的内核对象做两次快照,比较这两次的不同,就可以知道有哪些内核对象创建了,同时还能找到是在哪儿分配的。
打开windbg,按F6,附加报警服务器进程。在命令行里面输入!htrace–enable开启htrace功能。输入!htrace –snapshot做第一个快照,然后输入g命令,让程序执行一段时间。
该过程如下图所示:
程序运行一段时间后,按Ctrl+Break键,将程序中断下来。这时输入!handle –diff,可以比较新增句柄的分配上下文。
通过仔细观察,可以看出,有创建了多个线程内核对象,查看一个内核对象的信息可以使用!handle命令。例如查看handle值为f98的线程内核对象,可输入!handlef98 ff。如下图所示
其起始地址为HPR_GetNetWorkFlowData,可以确定该线程是使用HPR库创建的,可以通过在源代码中查找HPR_Thread_Create来缩小范围。
再图中还可以看到线程ID为7670.fef4,其中7670为进程ID,fef4是线程ID,因此可以切换到该线程看看,输入
~~[fef4]s命令,切换线程。
出现了一条错误信息,即该线程已经不存在了,那其实我们就基本可以确定这里发生了泄露,线程已经不复存在,但该线程的内核对象却未能关闭。
通过仔细观察,我们可以看到这些线程对象都是同一个线程创建的,如图
最终都指向了71a8这个线程,这样我们可以确定是71a8这个线程创建了很多线程内核对象,却没有将内核对象的句柄关闭,这样造成了线程内核对象的句柄泄露,只要定位到71a8这个线程,就能找到泄露的地方。
输入~~[71a8]s,切换到这个线程,并输入kb打印其调用栈,如下图:
调用栈是一堆非常奇怪的数据,其实是因为我的系统是64位的,而报警服务器这个进程是32位的,windbg使用64位的上下文去解析32位的进程,造成了错误的解析。这时可以通过!sw这条命令来切换至32位的上下文。然后再敲入kb命令,就能看到该线程的调用栈了,如图:
可以清楚的看到该线程的调用栈,此时该线程正在等待socket连接,使用的是HPR库的HPR_Accept函数,到这个地步,在源代码中定位该线程函数就非常容易了。
在源代码中搜索HPR_Accept函数,只有一个地方:
可以清楚的看到,这个线程接受到连接之后,就创建了一个线程,不过却未将创建线程的句柄关闭,从而造成了句柄泄露,这与我们之前分析的一致。知道了问题所在,修改也就很方便了。