应优先使用Performance Counter的SystemHandle Count计数器获取近似全局句柄总数,它无需提权、线程安全且轻量;若需精确值则调用NtQuerySystemInformation配合SystemExtendedHandleInformation与SYSTEM_HANDLE_INFORMATION_EX结构体,并注意启用SE_DEBUG_NAME特权、动态分配缓冲区及正确处理结构体对齐。

Windows平台下用NtQuerySystemInformation获取句柄总数
Windows内核并未暴露“全局句柄数”的标准API,GetProcessHandleCount只返回当前进程的句柄数,而系统级统计必须走NT层接口。实际可行的是调用未公开但长期稳定的NtQuerySystemInformation,传入SystemHandleInformation(值为16)——它会返回所有进程持有的句柄快照,遍历后累加即可得到全局总数。
注意:该函数在ntdll.dll中,需手动LoadLibrary和GetProcAddress;且返回结构体SYSTEM_HANDLE_INFORMATION在不同Windows版本中字段偏移有差异(Win10 20H1后新增ObjectPointer字段),直接按旧结构体解析会导致越界读取或崩溃。
- 必须使用
sizeof(SYSTEM_HANDLE_INFORMATION_EX)(带EX后缀的变体)配合SystemExtendedHandleInformation(值为64)更稳妥,该结构体自Win8起稳定,包含NumberOfHandles字段,可直接读取总数,无需遍历 - 调用前需启用
SE_DEBUG_NAME特权,否则多数进程句柄信息返回STATUS_ACCESS_DENIED - 返回缓冲区大小不确定,需先用0长度试探,再分配足够内存重试——常见错误是固定分配4KB导致截断
用Performance Counter直接读取Handle Count性能计数器
如果只需要近似实时总数(精度±1s,无权限要求),\System\Handle Count这个性能计数器最轻量。它由Windows内核周期性更新,本质是NtQuerySystemInformation结果的缓存值,但无需提权、不触发AV误报、线程安全。
实操时用PdhOpenQuery + PdhAddCounter + PdhCollectQueryData三步即可,比NT API简单得多,适合监控服务长期运行。
立即学习“C++免费学习笔记(深入)”;
-
PdhAddCounter的参数szCounterPath必须严格为"\System\Handle Count",写成"\System\Handles"或漏掉反斜杠会返回PDH_INVALID_COUNTER_PATH - 首次
PdhCollectQueryData返回PDH_INVALID_DATA是正常现象,需等待至少一次采样周期(默认1s)后再读值 - 该计数器返回的是
PDH_FMT_LONG类型整数,不是浮点;若用PDH_FMT_DOUBLE格式化会得到0
避免句柄泄漏:用Process Explorer验证采集结果是否可信
代码里算出的总数和任务管理器/资源监视器显示的“句柄数”常有几十到几百的偏差,这不是bug,而是统计口径不同:你的程序遍历的是内核对象句柄表,而UI工具(如Task Manager)默认只显示用户模式可枚举的句柄(过滤了内核驱动创建的ALPC Port、Keyed Event等)。
真正需要关注的是趋势而非绝对值——如果每小时增长500+且不回落,基本可判定泄漏。此时应立刻用Process Explorer(Sysinternals套件)附加到可疑进程,按Handle列排序,看是否有大量重复类型(如Event、Section)持续增加。
- Process Explorer需以管理员身份运行才能看到完整句柄列表
- 右键句柄 →
Properties→Stack标签页可查看创建该句柄的调用栈(需提前配置符号服务器),这是定位泄漏源头的关键 - 不要依赖
CloseHandle返回值判断是否关闭成功:对已关闭句柄重复调用会返回FALSE且GetLastError为ERROR_INVALID_HANDLE,但不影响统计总数
C++实现时绕不开的结构体定义与编译兼容性问题
微软头文件(winternl.h)不公开SYSTEM_HANDLE_INFORMATION_EX,必须手写定义。最容易踩的坑是结构体打包(packing)——x64下默认8字节对齐,但NT结构体是自然对齐(ULONG占4字节,PVOID占8字节),若用#pragma pack(1)强制1字节对齐,会导致字段错位、读出垃圾值。
正确做法是用#pragma pack(push, 8)保持默认对齐,并严格按文档顺序声明字段:
struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX {
PVOID Object;
ULONG_PTR UniqueProcessId;
ULONG_PTR HandleValue;
ULONG GrantedAccess;
USHORT CreatorBackTraceIndex;
USHORT ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
};
struct SYSTEM_HANDLE_INFORMATION_EX {
ULONG_PTR NumberOfHandles;
ULONG_PTR Reserved;
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1];
};
另外,ULONG_PTR在x64下是unsigned long long,但某些旧版MinGW或交叉编译环境可能未正确定义,需加#ifdef _WIN64保护。
真实场景中,句柄总数本身意义有限;关键在建立基线、识别异常增速、关联到具体进程——这些都得靠多次采样+差分计算,而不是单次调用就下结论。


















