ReadFile在结束从子进程读取stdout时不会返回



我正在开发我的库,该库需要在运行时捕获和处理子进程的标准输出(和错误)。当ReadFile用于读取输出时,问题就出现了,一旦进程结束(被终止或退出),它就不会返回。

看起来ReadFile无法检测到管道的另一端(写句柄)已关闭。根据文档,它应该返回FALSE,并将最后一个错误设置为ERROR_BROKEN_PIPE:

如果正在使用匿名管道,并且写入句柄已关闭,当ReadFile尝试使用管道的相应读取句柄进行读取时,函数将返回FALSE,GetLastError将返回ERROR_BROKEN_pipe。

这是我的代码,我已经去掉了不相关的部分:(注意:我已经更新了allium_start以遵循建议的更改,我保留原始代码供参考,请使用更新的功能代码来查找缺陷)

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
// Prepare startup info with appropriate information
SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
instance->startup_info.dwFlags = STARTF_USESTDHANDLES;
SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};
HANDLE pipes[2];
if (output_pipes == NULL) {
CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
output_pipes = pipes;
}
instance->startup_info.hStdOutput = output_pipes[1];
instance->startup_info.hStdError = output_pipes[1];
instance->stdout_pipe = output_pipes[0]; // Stored for internal reference
// Create the process
bool success = CreateProcessA(
NULL,
cmd,
NULL,
NULL,
config ? true : false,
0,
NULL,
NULL,
&instance->startup_info,
SecureZeroMemory(&instance->process, sizeof instance->process)
);
// Return on failure
if (!success) return false;
}
char *allium_read_stdout_line(struct TorInstance *instance) {
char *buffer = instance->buffer.data;
// Process the input
unsigned int read_len = 0;
while (true) {
// Read data
unsigned long bytes_read;
if (ReadFile(instance->stdout_pipe, buffer, 1, &bytes_read, NULL) == false || bytes_read == 0) return NULL;
// Check if we have reached end of line
if (buffer[0] == 'n') break;
// Proceed to the next character
++buffer; ++read_len;
}
// Terminate the new line with null character and return
// Special handling for Windows, terminate at CR if present
buffer[read_len >= 2 && buffer[-1] == 'r' ? -1 : 0] = '';
return instance->buffer.data;
}

allium_start创建用于输出重定向的管道(它对stdout和stderr使用相同的管道来获得合并流),然后创建子进程。另一个allium_read_stdout_line函数负责读取管道的输出,并在遇到新行时返回。

这个问题发生在ReadFile函数调用中,如果进程退出后没有什么可读取的内容,它将永远不会返回,据我所知,进程结束时,所有句柄都会被Windows关闭,所以看起来ReadFile无法检测到另一端的管道(写句柄)已经关闭的事实。

我该如何解决这个问题?我一直在寻找解决方案,但到目前为止还没有找到,一个潜在的选择是使用多线程并将ReadFile放在一个单独的线程中,这样它就不会阻塞整个程序,通过使用这种方法,我可以在等待读取完成时定期检查进程是否仍然存在。。。或者在进程结束时终止/停止线程。

我确实更喜欢解决这个问题,而不是选择变通方法,但我对任何其他解决方案都持开放态度。提前感谢!


编辑:在阅读@RemyLebeau的回答和@RbMm在该回答中的评论后,很明显,我对处理继承的理解存在根本缺陷。因此,我将他们的建议(SetHandleInformation禁用读取句柄的继承,并在创建子进程后关闭它)纳入了我的allium_start函数:

bool allium_start(struct TorInstance *instance, char *config, allium_pipe *output_pipes) {
// Prepare startup info with appropriate information
SecureZeroMemory(&instance->startup_info, sizeof instance->startup_info);
instance->startup_info.dwFlags = STARTF_USESTDHANDLES;
SECURITY_ATTRIBUTES pipe_secu_attribs = {sizeof(SECURITY_ATTRIBUTES), NULL, true};
HANDLE pipes[2];
if (output_pipes == NULL) {
CreatePipe(&pipes[0], &pipes[1], &pipe_secu_attribs, 0);
output_pipes = pipes;
}
SetHandleInformation(output_pipes[0], HANDLE_FLAG_INHERIT, 0);
instance->startup_info.hStdOutput = output_pipes[1];
instance->startup_info.hStdError = output_pipes[1];
instance->stdout_pipe = output_pipes[0]; // Stored for internal reference
// Create the process
bool success = CreateProcessA(
NULL,
cmd,
NULL,
NULL,
config ? true : false,
0,
NULL,
NULL,
&instance->startup_info,
SecureZeroMemory(&instance->process, sizeof instance->process)
);
// Close the write end of our stdout handle
CloseHandle(output_pipes[1]);
// Return on failure
if (!success) return false;
}

(以下文本最初出现在编辑2之前)

但遗憾的是,它仍然不起作用:(

编辑2(接受答案后):确实有效!请参阅我对已接受答案的最后评论。

您没有正确管理管道,或者更具体地说,您没有控制管道句柄的继承。不要让子进程继承管道(output_pipes[0])的读取句柄,否则子进程结束时管道将无法正确断开。

阅读MSDN了解更多详细信息:

创建具有重定向输入和输出的子进程

重定向的标准句柄即使子进程已退出也不会关闭的情况

使用SetHandleInformation()PROC_THREAD_ATTRIBUTE_LIST可防止CreateProcess()output_pipes[0]作为可继承句柄传递给子进程。子进程不需要访问该句柄,因此无论如何都不需要通过进程边界传递该句柄。它只需要访问管道的写入句柄(output_pipes[1])。

对于匿名管道,读取进程和写入进程将具有hRead和hWrite的处理程序,每个进程都有自己的处理程序(继承后复制)。因此,在您的子进程退出并关闭其中的处理程序后,另一个hWrite仍在父进程中。我们必须注意在写过程中关闭hRead,在读过程中关闭h write。

我可以重现这个ReadFile问题,如果在设置子进程的hStdOutputhStdError后关闭写处理程序,则在子进程退出后,ReadFile将返回0。

这是我的代码示例,Parent.cpp:

#include <windows.h> 
#include <iostream>
#include <stdio.h>
HANDLE childInRead = NULL;
HANDLE W1 = NULL;
HANDLE W2 = NULL;
HANDLE R2 = NULL;
HANDLE R1 = NULL;
#define BUFSIZE 4096
void CreateChildProcess() {
TCHAR applicationName[] = TEXT("kids.exe");
PROCESS_INFORMATION pi;
STARTUPINFO si;
BOOL success = FALSE;
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.hStdError = W1;
si.hStdOutput = W1;
si.hStdInput = R2;
si.dwFlags |= STARTF_USESTDHANDLES;
success = CreateProcess(NULL, applicationName, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (!success) {
printf("Error creating child process n");
}
else {
printf("Child process successfuly created n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
}
int main()
{
printf("Parent process running.... n");
DWORD dRead, dWritten;
CHAR chBuf[BUFSIZE] = { 0 };
BOOL bSuccess = FALSE;
SECURITY_ATTRIBUTES secAttr;
secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
secAttr.bInheritHandle = TRUE;
secAttr.lpSecurityDescriptor = NULL;
printf("Creating first pipe n");
if (!CreatePipe(&R1, &W1, &secAttr, 0)) {
printf("n error creating first pipe n");
}
printf("Creating second pipe n");
if (!CreatePipe(&R2, &W2, &secAttr, 0)) {
printf("n error creating second pipe n");
}
if (!SetHandleInformation(R1, HANDLE_FLAG_INHERIT, 0)) {
printf("n R1 SetHandleInformation n");
}
if (!SetHandleInformation(W2, HANDLE_FLAG_INHERIT, 0)) {
printf("n W1 SetHandleInformation n");
}
printf("n Creating child process..... n");
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE);
CreateChildProcess();
CloseHandle(W1);
CloseHandle(R2);
for (;;) {
printf("Inside for loop n");
//1. read from stdin
printf("read from stdin:n");
bSuccess = ReadFile(hStdIn, chBuf, BUFSIZE, &dRead, NULL);
if (!bSuccess) {
printf("error reading n");
break;
}

//2. write to Pipe2
printf("write to Pipe2...n");
bSuccess = WriteFile(W2, chBuf, 100, &dWritten, NULL);
if (!bSuccess) {
printf("error reading n");
break;
}
//3. read from Pipe1
printf("read from Pipe1...n");
bSuccess = ReadFile(R1, chBuf, BUFSIZE, &dRead, NULL);
if (!bSuccess)
{
printf("error reading :%d n", GetLastError());
break;
}
//4. write to stdout
printf("write to stdout:n");
bSuccess = WriteFile(hStdOut, chBuf, 100, &dWritten, NULL);
if (!bSuccess) {
printf("error reading n");
break;
}
}
getchar();
return 0;
}

Kids.pp:

#include <windows.h>
#include <stdio.h>
#define BUFSIZE 4096
int main()
{
DWORD dRead, dWritten;
CHAR chBuf[BUFSIZE];
BOOL success = FALSE;
HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
printf("Child process running....");
if (stdIn == INVALID_HANDLE_VALUE || stdOut == INVALID_HANDLE_VALUE) {
ExitProcess(1);
}
//for (;;) {
success = ReadFile(stdIn, chBuf, BUFSIZE, &dRead, NULL);
//if (!success || dRead == 0) break;
success = WriteFile(stdOut, chBuf, dRead, &dWritten, NULL);
//if (!success) break;
//}
return 0;
}

最新更新