Windows进程退出后资源未释放:JOB_OBJECT_MSG_EXIT_PROCESS通知问题
构建系统进程退出后的资源竞争问题
问题流程
- 父进程创建子进程;
- 子进程创建文件后未关闭句柄即退出;
- 父进程等待
JOB_OBJECT_MSG_EXIT_PROCESS通知以确认子进程退出; - 父进程尝试打开子进程创建的文件时,出现
ERROR_SHARING_VIOLATION错误。
现象分析
通过Windows Performance Analyzer追踪发现,文件打开操作发生在子进程生命周期结束后,但文件清理操作仍在进行。此时进程处于僵尸状态,进程句柄尚未被触发,使用WaitForSingleObject进行阻塞等待可解决该竞争问题。
该现象在多台Windows 10/11系统中均一致,即使禁用杀毒软件也是如此。这一情况超出预期,似乎降低了JOB_OBJECT_MSG_EXIT_PROCESS通知的实用性。
疑问
Windows是否存在进程已退出但API可见的副作用仍在持续的状态?若存在,是否必须等待进程句柄被触发,才能可靠地等待该“僵尸状态”结束?
(注:等待JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO似乎也能解决问题,但在本应用场景中并不适用。)
复现代码
/** * Repro case demonstrating resources held after JOB_OBJECT_MSG_EXIT_PROCESS */ #include <stdio.h> #include <windows.h> // Test condition and panic out after tracing the offending line number on error #define CHECK(cond) (check_at((cond), __LINE__)) static void check_at(_Bool cond, unsigned int line) { if(!cond) { fprintf(stderr, "%s:%u: failed\n", __FILE__, line); ExitProcess(1); } } // Operate as child process create the file and leave cleaning up to the OS static void child(const WCHAR filename[]) { HANDLE handle = CreateFileW(filename, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); CHECK(handle != INVALID_HANDLE_VALUE); static const char payload[0x10000 /* ≥5 bytes required */]; DWORD written; CHECK(WriteFile(handle, payload, sizeof payload, &written, NULL)); CHECK(written == sizeof payload); } // Operate as parent executing the child and waiting for completion before // accessing the file static const char *parent(const WCHAR filename[], WCHAR app_name[], BOOL poll) { // Use job object reporting status to an I/O completion port to wait for exit. // Plain WaitForSingleObject on the child fails to reproduce the issue HANDLE job = CreateJobObjectW(NULL, NULL); CHECK(job); HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); CHECK(iocp); JOBOBJECT_ASSOCIATE_COMPLETION_PORT assoc = { .CompletionPort = iocp }; CHECK(SetInformationJobObject(job, JobObjectAssociateCompletionPortInformation, &assoc, sizeof assoc)); // Associate job with queue immediately from startup (Windows ≥10 required) STARTUPINFOEXW si = { .StartupInfo.cb = sizeof si }; size_t space = 0; while(!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &space)) si.lpAttributeList = _alloca(space); DWORD proc_attrib = 0x0002000DU /* PROC_THREAD_ATTRIBUTE_JOB_LIST */; CHECK(UpdateProcThreadAttribute(si.lpAttributeList, 0, proc_attrib, &job, sizeof job, NULL, NULL)); // Pass a second dummy command line argument to trigger child behavior WCHAR command_line[] = L"dummy recurse"; PROCESS_INFORMATION pi; CHECK(CreateProcessW(app_name, command_line, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)); // Wait for successful process to exit DWORD event; OVERLAPPED *overlapped; do { ULONG_PTR key; CHECK(GetQueuedCompletionStatus(iocp, &event, &key, &overlapped, INFINITE)); } while(event != JOB_OBJECT_MSG_EXIT_PROCESS && event != JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS); CHECK((ULONG_PTR) overlapped == pi.dwProcessId); // Poll and verify yet to be signaled or block until signaled if(poll) CHECK(WaitForSingleObject(pi.hProcess, 0) != WAIT_OBJECT_0); else CHECK(WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0); // Verify successful exit DWORD exit_code; CHECK(GetExitCodeProcess(pi.hProcess, &exit_code)); CHECK(exit_code == 0); // Try to read the expected output file HANDLE handle = CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(handle != INVALID_HANDLE_VALUE) { CHECK(CloseHandle(handle)); return "PASS"; } else { CHECK(GetLastError() == ERROR_SHARING_VIOLATION); return "FAIL"; } } int wmain(int argc, WCHAR *argv[]) { const WCHAR *filename = L"shared_file"; if(argc == 1) { printf("polling: %s\n", parent(filename, argv[0], TRUE)); printf("blocking: %s\n", parent(filename, argv[0], FALSE)); } else { child(filename); } return 0; }
内容的提问来源于stack exchange,提问作者doynax
相关产品推荐
相关产品推荐

