C/C++ · 2025年5月23日

CreatePipe 创建管道发送数据, 子进程读取会造成阻塞

主要原因是创建pipe时,将写句柄继承给了子进程,因此在父进程中即使关掉写句柄,那么自己成还是有该句柄的所有权,因此并没有关闭。所以在子进程中从 stdin 读数据,会造成阻塞。
在父进程中,创建pipe时或创建后,设置子进程不可继承写句柄,即可解决该问题。

int main(int arg, char* argv[])
{
    int ret = 0;
    SECURITY_ATTRIBUTES saAttr;
    HANDLE hChildStd_IN_Rd = NULL;
    HANDLE hChildStd_IN_Wr = NULL;
    PROCESS_INFORMATION piProcInfo;
    STARTUPINFO siStartInfo;
    BOOL bSuccess = FALSE;
    DWORD bytesWritten;

    // 传送给外部 exe 的参数
    char* cmd = NULL;
    // 使用 windows API 是以宽字符传参, 故需要进行格式转换
    wchar_t* cmdWide = NULL;

    // argv[1]: 源文件路径; argv[2]: 保存路径
    if (arg < 3)
    {
        fprintf(stderr, "Invalid args\r\n");
        goto err;
    }

    // 为 cmd 分配内存, 视具体参数而定
    cmd = calloc(256, sizeof(char));
    if (cmd == NULL)
    {
        ret = -1;
        goto err;
    }

    // windows.h 中的 API, 均已 宽字符 传参, 因此需要转换为宽字符
    sprintf(cmd, "--stable --hash -s %s -d %s", argv[1], argv[2]);
    int len = MultiByteToWideChar(CP_ACP, 0, cmd, -1, NULL, 0);
    cmdWide = calloc(len, sizeof(wchar_t));
    if (cmdWide == NULL)
    {
        ret = -1;
        goto err;
    }
    MultiByteToWideChar(CP_ACP, 0, cmd, -1, cmdWide, len);

    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    saAttr.bInheritHandle = TRUE;
    saAttr.lpSecurityDescriptor = NULL;

    if (!CreatePipe(&hChildStd_IN_Rd, &hChildStd_IN_Wr, &saAttr, 0))
    {
        printf("CreatePipe for STDIN failed\n");
        goto err;
    }

    // 注意要将 hChildStd_IN_Wr 设置为子进程不可继承
    // 否则在附近中即使关闭了句柄, 但子进程还拥有其所有权, 因此子进程中使用 fread 等读取数据会阻塞 
    SetHandleInformation(hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0);
    SetHandleInformation(hChildStd_IN_Rd, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

    ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
    siStartInfo.cb = sizeof(STARTUPINFO);
    siStartInfo.hStdInput = hChildStd_IN_Rd;
    siStartInfo.dwFlags |= STARTF_USESTDHANDLES;

    bSuccess = CreateProcess(
        TEXT(".\\third_tool.exe"),
        cmdWide,
        NULL,
        NULL,
        TRUE,
        0,
        NULL,
        NULL,
        &siStartInfo,
        &piProcInfo
    );
    if (!bSuccess)
    {
        printf("CreateProcess failed\n");
        goto err;
    }

    // 释放子进程 读句柄, 父进程中并不会用到此
    CloseHandle(hChildStd_IN_Rd);

    if (!WriteFile(hChildStd_IN_Wr, cmd, strlen(cmd), &bytesWritten, NULL))
    {
        printf("WriteFile failed, error: %d\n", GetLastError());
        goto err;
    }

    if (!FlushFileBuffers(hChildStd_IN_Wr)) {
        goto err;
    }

    // 关闭写句柄时, 子进程中会读到 EOF, 读数据终止, 故一定要在写入完成后, 关闭写句柄
    CloseHandle(hChildStd_IN_Wr);
    hChildStd_IN_Wr = NULL;

    // 等待子进程结束
    WaitForSingleObject(piProcInfo.hProcess, INFINITE);

    goto end;

err:
    ret = (ret != 0) ? ret : -1;

end:
    // 释放内存等
    if (cmd)
    {
        free(cmd);
    }
    if (cmdWide)
    {
        free(cmdWide);
    }

    if (hChildStd_IN_Wr) CloseHandle(hChildStd_IN_Wr);

    if (piProcInfo.hProcess)
    {
        CloseHandle(piProcInfo.hProcess);
    }
    CloseHandle(piProcInfo.hThread);
    return 0;
}