为何处于挂起状态的monitor程序内存占用持续增长?
问题背景
开发了一个C++后台监控程序monitor,功能是监控目标程序target,当target退出时自动重启它。测试阶段用notepad.exe作为目标程序,实际场景中target是一款联网GUI应用,正常情况下不会主动退出,因此monitor会长期处于挂起等待状态。但观察发现,即使程序处于挂起状态,其内存占用仍呈长期增长趋势。
程序未显式使用new()或malloc()分配内存,仅在调用CreateToolhelp32Snapshot()和OpenProcess()时涉及系统资源分配,且已通过CloseHandle()释放对应句柄,理论上不应出现内存泄漏。
监控程序代码
#include <windows.h> #include <tlhelp32.h> #include <Process.h> void BindToProcess(); const WCHAR PATH[] = L"C:\\Windows\\System32\\notepad.exe"; const WCHAR EXE[] = L"notepad.exe"; int CALLBACK WinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) { do { BindToProcess(); _wspawnl(_P_WAIT, PATH, PATH, NULL); } while (TRUE); return 0; } void BindToProcess() { HANDLE hProcessSnap; PROCESSENTRY32 pe32; // Take a snapshot of all processes in the system. hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hProcessSnap == INVALID_HANDLE_VALUE) return; // Set the size of the structure before using it. pe32.dwSize = sizeof(PROCESSENTRY32); // Retrieve information about the first process, if (!Process32First(hProcessSnap, &pe32)) { CloseHandle(hProcessSnap); return; } // Now walk the snapshot of processes, and search for target do { const int len = (int) wcsnlen_s(EXE, 10); if (CompareStringW(0, 0, pe32.szExeFile, len, EXE, len) == CSTR_EQUAL) { HANDLE handle[1]; handle[0] = OpenProcess(SYNCHRONIZE, FALSE, pe32.th32ProcessID); WaitForMultipleObjects(1, handle, TRUE, INFINITE); CloseHandle(handle[0]); CloseHandle(hProcessSnap); return; } } while (Process32Next(hProcessSnap, &pe32)); CloseHandle(hProcessSnap); return; }
内存增长的核心原因分析
1. GUI程序缺少消息循环,导致消息队列堆积
程序使用WinMain作为入口,表明它属于Windows GUI子系统。Windows会为GUI程序自动创建消息队列,系统会定期向程序发送各类消息(如WM_NULL、系统通知等),即使程序没有创建窗口。
而当前程序在WaitForMultipleObjects(...)处长期挂起,完全没有处理消息的逻辑,导致系统发送的消息不断堆积在消息队列中。每一条未处理的消息都会占用内存空间,随着时间推移,内存占用自然会持续增长。
2. Debug编译版本的CRT内存调试机制(次要原因)
如果程序是用Debug模式编译的,Visual Studio的C运行时库(CRT)会为每一次内存分配添加额外的调试跟踪信息。即使调用了CloseHandle()释放系统资源,CRT的堆管理器可能不会立即将内存归还给操作系统,而是保留在进程的堆空间中用于后续复用,这会导致内存占用看起来持续上升。这种情况在Release模式下会显著缓解。
3. 系统工作集的波动(次要原因)
Windows系统会根据进程的运行情况动态调整其工作集(Working Set)大小,可能会预分配一些内存页面或保留常用页面在内存中,导致内存占用出现小幅波动。但这种波动通常是双向的,不会呈现长期持续增长的趋势,因此不是核心原因。
解决方案
方案一:改为控制台子系统程序
将程序入口从WinMain改为main,编译时指定控制台子系统。这样Windows不会为程序创建GUI消息队列,从根源上避免消息堆积问题。
方案二:在等待时处理消息
将WaitForMultipleObjects替换为MsgWaitForMultipleObjects,在等待目标进程退出的同时,处理系统发送的消息。示例修改如下:
// 替换BindToProcess中的WaitForMultipleObjects代码段 HANDLE handle[1]; handle[0] = OpenProcess(SYNCHRONIZE, FALSE, pe32.th32ProcessID); // 循环等待,同时处理消息 DWORD waitResult; do { waitResult = MsgWaitForMultipleObjects(1, handle, FALSE, INFINITE, QS_ALLINPUT); if (waitResult == WAIT_OBJECT_0 + 1) { // 处理所有待处理的消息 MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } } } while (waitResult != WAIT_OBJECT_0); CloseHandle(handle[0]); CloseHandle(hProcessSnap); return;
这样既能保持对目标进程的监控,又能及时处理系统消息,避免消息堆积导致的内存增长。
内容的提问来源于stack exchange,提问作者Kalle Svensson

