在仍保留资源的情况下发送 Windows 进程退出通知

问题描述 投票:0回答:1

我正在调试构建系统中的一个问题,其本质上是执行以下操作:

  1. 父进程生成子进程。
  2. 子进程创建文件并退出,但不关闭句柄。
  3. 父进程等待 JOB_OBJECT_MSG_EXIT_PROCESS 等待子进程退出。
  4. 父进程尝试打开子进程创建的文件。此操作失败并显示 ERROR_SHARING_VIOLATION。

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;
}
c windows process io-completion-ports
1个回答
0
投票

猜测#1:也许在子进程被清理之前文件不会被关闭和释放。即使子进程已经完成,父进程仍然持有打开的句柄。 (在调用

PROCESS_INFORMATION
时填写的
CreateProcess
结构具有子进程及其主线程的句柄。)在尝试打开文件之前,让父进程关闭子进程句柄。

猜测#2:反恶意软件软件看到新文件的创建并用锁打开它,直到扫描它。

© www.soinside.com 2019 - 2024. All rights reserved.