Windows平台捕获子进程stdout/stderr时遇阻塞问题求助
问题
我尝试捕获通过CreateProcess启动的Windows子进程的stdout和stderr,操作步骤如下:
- 创建命名管道(FIFO)用于stdout和stderr:
HANDLE hstdout; while ((hstdout = CreateNamedPipe( stdout_filename.c_str() , PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE , PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS , 10 , 4096 , 4096 , 0 , &sa)) == INVALID_HANDLE_VALUE) { // 若管道已被占用,则在名称后添加下划线并重试 stdout_filename += L"_"; }
stderr管道创建逻辑与stdout相同。
- 配置
STARTUPINFO并创建子进程:
PROCESS_INFORMATION pi = {}; ZeroMemory(&pi, sizeof(pi)); STARTUPINFO sui = { sizeof(STARTUPINFO) // size , 0 // reserved , nullptr // desktop , nullptr // title , 0, 0, 0, 0 // x, y, cx, cy , 0, 0 // x buffer, y buffer , 0 // fill attribute , STARTF_FORCEOFFFEEDBACK // flags | STARTF_USESTDHANDLES , false // show window , 0 // reserved , 0 // reserved , GetStdHandle(STD_INPUT_HANDLE) // stdin , hstdout // stdout , hstderr // stderr }; BOOL success = CreateProcess( nullptr // application name , &cmdline[0] // command line , nullptr // process attributes , nullptr // security attributes , true // inherit handles , CREATE_NO_WINDOW // creation flags | INHERIT_PARENT_AFFINITY , nullptr // environment , nullptr // current directory , &sui // startup info , &pi // process info );
- 等待管道和进程句柄并读取输出:
if (success) { WCHAR buffer[4096]; DWORD bytesRead; enum { eStdOut, eStdErr, eProcess }; HANDLE handles[] = { hstdout, hstderr, pi.hProcess }; DWORD waitResult; bool process_terminated = false; do { waitResult = WaitForMultipleObjects(_countof(handles), handles, TRUE, INFINITE); switch (waitResult) { case WAIT_TIMEOUT: log_fs << L"Error: Wait timed out\n"; break; case WAIT_FAILED: log_fs << L"Error: Wait failed\n"; break; case eStdOut + WAIT_OBJECT_0: // 注释有误,应为stdout触发信号 if (ReadFile(hstdout, buffer, _countof(buffer), &bytesRead, nullptr)) { log_fs.write(buffer, bytesRead); wcout.write(buffer, bytesRead); } else { log_fs << "Error reading from stdout" << endl << get_error() << endl; } break; case eStdErr + WAIT_OBJECT_0: // stderr触发信号 if (ReadFile(hstderr, buffer, _countof(buffer), &bytesRead, nullptr)) { log_fs.write(buffer, bytesRead); wcerr.write(buffer, bytesRead); } else { log_fs << "Error reading from stderr" << endl << get_error() << endl; } break; case eProcess + WAIT_OBJECT_0: // 进程已终止 log_fs << L"Process terminated" << endl; break; case eStdOut + WAIT_ABANDONED_0: case eStdErr + WAIT_ABANDONED_0: case eProcess + WAIT_ABANDONED_0: // 某个句柄被放弃 log_fs << L"Error: wait abandoned " << (waitResult - WAIT_ABANDONED_0) << endl << get_error() << endl; break; default: log_fs << L"Error: Unknown " << waitResult << " (" << (waitResult - WAIT_OBJECT_0) << ", " << (waitResult - WAIT_ABANDONED_0) << ")" << endl << get_error() << endl; break; } } while (waitResult != WAIT_OBJECT_0 + eProcess); if (!GetExitCodeProcess(pi.hProcess, &exitCode)) { log_fs << L"Error getting return code\n"; } CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
- 测试程序代码:
int main() { std::cout << "Hello World!\n"; std::cerr << "Hello Underworld!\n"; // std::wstring str; // std::wcin >> str; }
遇到的问题:
- 注释测试程序中读取stdin的代码时,会进入stdout读取错误分支,错误信息为“Waiting for a process to open the other end of the pipe.”,随后程序在
WaitForMultipleObjects处无限阻塞; - 取消注释测试程序中读取stdin的代码时,程序直接在
WaitForMultipleObjects处无限阻塞。
请问哪里操作错误?
分析与解决
核心错误点
管道未完成连接配对
你创建的是PIPE_WAIT模式的命名管道,这种模式下父进程作为服务器端,必须调用ConnectNamedPipe等待子进程打开管道句柄完成连接。直接把管道句柄传给子进程后跳过这一步,会导致管道两端未建立有效连接,后续读取时触发“等待另一端打开管道”的错误。WaitForMultipleObjects等待逻辑错误
调用时第三个参数传了TRUE,表示要等待所有句柄都触发信号才返回。但实际需求是任意一个句柄触发就处理,应该传FALSE。这也是取消stdin注释后死锁的原因:子进程阻塞等待stdin输入,父进程阻塞等待所有三个句柄触发,形成互相等待的死锁。stdin句柄未正确处理
直接将父进程的STD_INPUT_HANDLE传给子进程,当子进程尝试读取stdin时,父进程没有向该句柄写入数据,子进程会一直阻塞;同时父进程在等待所有句柄触发,最终死锁。
修复步骤
1. 补全管道连接逻辑
创建管道后立即调用ConnectNamedPipe等待子进程连接,处理stdout和stderr管道:
// 创建stdout管道后添加 if (!ConnectNamedPipe(hstdout, nullptr)) { // 管道已被连接则无需等待,否则处理错误 if (GetLastError() != ERROR_PIPE_CONNECTED) { CloseHandle(hstdout); return -1; } } // stderr管道同理 if (!ConnectNamedPipe(hstderr, nullptr)) { if (GetLastError() != ERROR_PIPE_CONNECTED) { CloseHandle(hstderr); CloseHandle(hstdout); return -1; } }
2. 修正WaitForMultipleObjects参数
将第三个参数改为FALSE,实现任意句柄触发即返回:
waitResult = WaitForMultipleObjects(_countof(handles), handles, FALSE, INFINITE);
3. 正确处理子进程stdin
如果不需要子进程读取stdin,直接传递无效句柄:
// 修改STARTUPINFO中的stdin设置 sui.hStdInput = INVALID_HANDLE_VALUE;
如果需要支持stdin,需创建双向管道并实现父进程写入逻辑,同时完成管道连接。
4. 区分管道正常关闭与错误
子进程关闭管道时,ReadFile会返回FALSE且错误码为ERROR_BROKEN_PIPE,这是正常结束信号,无需报错,可将该句柄标记为无效:
case eStdOut + WAIT_OBJECT_0: if (ReadFile(hstdout, buffer, _countof(buffer), &bytesRead, nullptr)) { log_fs.write(buffer, bytesRead); wcout.write(buffer, bytesRead); } else { DWORD err = GetLastError(); if (err == ERROR_BROKEN_PIPE) { // 子进程已关闭stdout,标记句柄无效 handles[eStdOut] = INVALID_HANDLE_VALUE; } else { log_fs << "Error reading from stdout" << endl << get_error() << endl; } } break;
stderr分支做相同处理,同时跟踪有效句柄数量,避免等待无效句柄。
5. 调整循环终止条件
循环不能仅判断进程是否结束,还要检查是否还有有效句柄需要等待:
int validHandles = _countof(handles); do { // ... 处理等待结果逻辑 ... // 更新有效句柄数 if ((waitResult == eStdOut + WAIT_OBJECT_0 || waitResult == eStdErr + WAIT_OBJECT_0) && GetLastError() == ERROR_BROKEN_PIPE) { validHandles--; } } while (!(waitResult == WAIT_OBJECT_0 + eProcess || validHandles == 0));
内容的提问来源于stack exchange,提问作者Adrian

