我正在调试构建系统中的一个问题,其本质上是执行以下操作:
Windows 性能分析器 跟踪显示,打开尝试发生在子进程生命周期结束后,而文件清理仍在进行中。
在这种僵尸状态下,进程句柄尚未发出信号,并且使用 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;
}
猜测#1:也许在子进程被清理之前文件不会被关闭和释放。即使子进程已经完成,父进程仍然持有打开的句柄。 (在调用
PROCESS_INFORMATION
时填写的 CreateProcess
结构具有子进程及其主线程的句柄。)在尝试打开文件之前,让父进程关闭子进程句柄。
猜测#2:反恶意软件软件看到新文件的创建并用锁打开它,直到扫描它。