在任务管理器和资源监视器中,我们能看到的进程状态除了已挂起,还有无响应。想想全屏游戏时,突然游戏无响应了,我们说这叫死机了,都只能心有不甘地重启电脑(当然是可以用快捷键启动任务管理器的)。
在Windows系统中,一个程序无响应的定义是: “如果应用程序未等待输入、未在启动处理中且未在5秒的内部超时期限内调用PeekMessage()
,则应用程序被视为未响应。”显然,这里的条件只适用于GUI程序,像没有消息循环的控制台程序也就无所谓无响应的概念了。
那么,如何判断某个进程是否属于无响应状态呢?我之前写过一篇博文win32 判断进程状态(挂起/运行中)、用API挂起/恢复进程,讲了判断进程状态的办法,提了一嘴无响应不算挂起。
Windows没有提供API给我们直接判断一个进程是否无响应,但既然在任务管理器可以看到这个状态,那么我们也就可以参考任务管理器的思路进行分析。
分析任务管理器
Windows 8以上版本的任务管理器是通过枚举所有窗口来进行判断的。
经过IDA分析,任务管理器进程监视的核心函数WdcProcessMonitor::Update()
检查并调用_CreateHangDetectionThread()
创建了专门用于监测进程状态的线程,线程函数HangDetectionThread()
又定期调用UpdateHangStatus()
,该函数调用了APIEnumDesktops()
枚举全部桌面,对于每个桌面,又调用了APIEnumDesktopWindows()
函数对窗口逐个判断。
判断窗口是否无响应时,调用了APIIsHungAppWindow()
,这是最中心的函数。尽管文档已经说明该函数不用于常规用途,之后可能更改或弃用,但是Windows 11最新的任务管理器仍在使用它,所以大可无视这个警告。
对于每个窗口,任务管理器的判断流程如下。
这里面用到了两个未公开函数,涉及了一个概念:幽灵窗口。
幽灵窗口
User32.dll
HWND WINAPI GhostWindowFromHungWindow(HWND hwndHung);
HWND WINAPI HungWindowFromGhostWindow(HWND hwndGhost);
我们常看到这样的现象,程序无响应时,窗口在点击时蒙上一层白色,此时窗口依然能够被拖动位置。按理说无响应窗口不会再处理消息,那么这个窗口是怎么回事?
事实上,在窗口无响应时,Windows桌面窗口管理器(dwm.exe)会在其上覆盖一个假窗口,以便用户移动或者最小化它。这种窗口叫做幽灵窗口(Ghost Window,官方译名为“重影窗口”),窗口类名为Ghost
,它复制了本体窗口的客户区内容以图片形式显示,本体窗口则被隐藏了。我们能够拖动的正是幽灵窗口。
有的情景需求是调用APIGetWindowFromPoint()
想要获取到对应位置的窗口句柄,但是对于无响应窗口,获取到的是它的幽灵窗口,想要正确找到目标,就需要HungWindowFromGhostWindow()
获取本体窗口,而另一个函数GhostWindowFromHungWindow()
功能相反,是根据本体窗口获取幽灵窗口的句柄。
不想要系统在窗口无响应时生成幽灵窗口,可以调用APIDisableProcessWindowsGhosting()
,它能够禁止当前进程窗口的“重影”特性。
判断进程是否无响应
参考任务管理器的逻辑,可以写出判断进程是否无响应的代码实现。要注意的是,这里的实例是极简型的,只判断单一进程(所以不涉及幽灵窗口),并且在目标进程的一个窗口无响应时,就认为整个进程无响应。如果需要更加扩展的功能,就可以参照上面所述的办法枚举所有桌面,再枚举桌面上的所有窗口,用任务管理器的完整思路来进行判断。
#include<Windows.h>
struct WNDINFO{
DWORD pid;
bool bNotResponding;
}; //定义用来传输数据的信息结构
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
WNDINFO* info = (MW_INFO*)lParam; DWORD pid;
//过滤不属于目标进程的窗口
GetWindowThreadProcessId(hwnd, &pid);
if(pid != info->pid)return TRUE;
//判断流程
info->bNotResponding = false;
HWND hOwner = GetWindow(hwnd, GW_OWNER);
LONG l = GetWindowLong(hwnd, GWL_EXSTYLE);
if((!hOwner || !IsWindowVisible(hOwner) || (l & WS_EX_APPWINDOW))
&& (l & WS_EX_TOOLWINDOW) == 0 && IsHungAppWindow(hwnd)){
info->bNotResponding = true;
return FALSE;
}
return TRUE;
}
//用该函数判断进程是否无响应
BOOL IsProcessNotResponding(DWORD pid){
//这个结构作为信息交换的载体
WNDINFO info = {}; info.pid = id;
EnumWindows(EnumWindowsProc, LPARAM(&info));
return info.bNotResponding;
}