无论是在编写Windows程序还是Linux程序,都可能存在句柄泄露的问题。在Linux中一般来说一个进程的fd使用是有上限的,可以使用ulimit
命令进行上限查看,当出现fd泄露的时候,可能会出现socket
创建失败,文件打不开等问题。Windows类似,本文主要阐述了对Windows中的句柄泄露的追踪方法。
Windows句柄泄露
在Windows开发中,当调用Windows API,比如CreateFile
, CreateEvent
, CreateThread
等API的时候,都会返回一个句柄Handle
。当相应的资源使用完后,如果没有调用CloseHandle
去关闭Handle,则会出现句柄泄露的问题。当这个问题发生的时候,当前进程再调用比如CreateThread
会返回Windows Error 1450
, 表示Insufficient system resources exist to complete the requested service.
,导致程序运行问题。Windows的总句柄数,也是有限制的,此时甚至会影响其他进程的运行。那么接下来让我们来看看如何定位句柄泄露问题吧。
Process Explorer定位句柄泄露
在任务管理器中可以查看一个进程的句柄数量,在Process Explorer
中也可以。我们可以这样去定位句柄泄露问题:
- 可以在
Process Explorer
中显示Handles
一列,如果进程有句柄泄露问题,那么这个进程的Handles
一列的数值会持续的增长 - 选中相应的进程,可以观察本进程的句柄详细信息。比如这个句柄,关联的是线程、文件、Event等等。
- 当出现句柄泄露的时候,那么会有大量的相似的类型的句柄出现在其中。
如果因为CreateThread
的句柄没有释放,导致句柄泄露,那么则可以在句柄详细信息的条目中看到很多Thread
类型的。然后查找可能调用CreateThread
的代码。
如果因为CreateFile
的句柄没有释放,则可以在Process Explorer
中查看文件的路径,根据文件的路径来查找可能引起句柄泄露的代码。
这种方式可以解决一部分句柄泄露问题,但是有时候可能碰到一些场景不能解决:
- 一个产品可能依赖于多个第三方模块,当句柄泄露的问题是第三方模块引起的,可能看到泄露句柄类型和名字也难以定位到具体的模块。
Process Explorer
不能够显示所有的句柄,比如无名的Event,这样也无法查找。
Windbg定位句柄泄露问题
除了上一章末讲的两个问题,那么有没有一种方法可以定位到这个泄露的句柄申请的地方吗?Windbg
就可以做到。
先上一段测试的Sample, 每隔一秒钟创建一个Event,但并没有调用CloseHandle
, 会导致Handle Leak。
#include <windows.h>
#include <iostream>
#include <thread>
void HandleLeak()
{
int iCount = 0;
while(true)
{
iCount++;
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD dwError = GetLastError();
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!hEvent || dwError != ERROR_SUCCESS)
{
std::cerr << "Number: " << iCount << " Error: "<< dwError << std::endl;
return;
}
}
}
int main()
{
HandleLeak();
return 0;
}
第一步
用Windbg attach到你要测试进程
第二步
Windbg中调用命令!htrace -enable
: 开启句柄追踪,并且保存当前所有的Handle的快照(Snapshot)
0:006> !htrace -enable
Handle tracing enabled.
Handle tracing information snapshot successfully taken.
第三步
Windobg中调用命令g
, 让程序运行一段时间
第四步
菜单Debug
->break
进入调试,Windbg中运行!htrace -diff
: 将进程当前的所有的句柄和之前快照的句柄进行对比,找出这段时间内多出来的句柄。
0:006> !htrace -diff
Handle tracing information snapshot successfully taken.
0x31 new stack traces since the previous snapshot.
Ignoring handles that were already closed...
Outstanding handles opened since the previous snapshot:
--------------------------------------
Handle = 0x0000000000000290 - OPEN
Thread ID = 0x0000000000001ca0, Process ID = 0x0000000000004360
0x00007ffca4dcb2a4: ntdll!NtCreateEvent+0x0000000000000014
0x00007ffca1ebb623: KERNELBASE!CreateEventA+0x0000000000000083
0x00007ff7ea001e94: HandleLeak!HandleLeak+0x0000000000000034
0x00007ff7ea002099: HandleLeak!main+0x0000000000000009
0x00007ff7ea0023d4: HandleLeak!__scrt_common_main_seh+0x000000000000010c
0x00007ffca23f4034: KERNEL32!BaseThreadInitThunk+0x0000000000000014
0x00007ffca4da3691: ntdll!RtlUserThreadStart+0x0000000000000021
--------------------------------------
Handle = 0x0000000000000280 - OPEN
Thread ID = 0x0000000000001ca0, Process ID = 0x0000000000004360
0x00007ffca4dcb2a4: ntdll!NtCreateEvent+0x0000000000000014
0x00007ffca1ebb623: KERNELBASE!CreateEventA+0x0000000000000083
0x00007ff7ea001e94: HandleLeak!HandleLeak+0x0000000000000034
0x00007ff7ea002099: HandleLeak!main+0x0000000000000009
0x00007ff7ea0023d4: HandleLeak!__scrt_common_main_seh+0x000000000000010c
0x00007ffca23f4034: KERNEL32!BaseThreadInitThunk+0x0000000000000014
0x00007ffca4da3691: ntdll!RtlUserThreadStart+0x0000000000000021
第五步
通过上述的Handle调用栈,就很容易能够知道导致句柄泄露的代码了。
以上方法可以比较完美的解决句柄泄露问题,但是如果问题本地难以重现,需要到客户环境中查找句柄泄露问题,那么一般不太建议Symbols拷贝到客户的环境中,以免造成Symbols泄露。那么上述第四步
中就无法查看到明确的函数调用栈,可以从客户环境中拷贝出来第四步
中!htrace -diff
的信息,然后再自己本地Load Symbols后通过ln HandleLeak+0x....
查看相应的函数调用栈,从而定位问题。
除了以上方法,还有一些可以在Windbg中直接查看Handle的方式来查找句柄泄露:
- 通过
!handle
命令查看当前进程的所有句柄 - 通过对比两个时间点的
!handle
命令的结果,找出句柄泄露的类型 - 找出两个时间点差异化的句柄的索引,再使用
!handle <handle index> f
查看句柄的详细信息。 - 通过泄露的句柄的类型,详细信息(比如名称)来辅助定位可能的句柄泄露位置
最后是个人微信公众号,文章CSDN和微信公众号都会发,欢迎一起讨论。