ReadFile 在结束后从子进程读取 stdout 时不 return
ReadFile does not return while reading stdout from a child process after it ends
我正在开发我的库,它需要在运行时捕获和处理子进程的标准输出(和错误)。当 ReadFile 用于读取输出时出现问题,它不会 return 一旦进程结束(被杀死或退出)。
看起来 ReadFile
无法检测到管道的另一端(写句柄)已关闭。根据文档,它应该 return FALSE
并将最后一个错误设置为 ERROR_BROKEN_PIPE
:
If an anonymous pipe is being used and the write handle has been closed, when ReadFile attempts to read using the pipe's corresponding read handle, the function returns FALSE and GetLastError returns 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] = '[=11=]';
return instance->buffer.data;
}
allium_start
为输出重定向创建管道(它对 stdout 和 stderr 使用相同的管道来获取合并流),然后创建子进程。另一个 allium_read_stdout_line
函数负责读取管道的输出并在遇到新行时 return 对其进行处理。
问题出现在 ReadFile
函数调用中,如果进程退出后没有任何可读取的内容,它永远不会 returns,根据我的理解,进程的所有句柄都被 Windows 当它结束时,所以看起来 ReadFile
无法检测到另一端的管道(写句柄)已关闭的事实。
我该如何解决这个问题?我一直在寻找解决方案,但到目前为止我已经找到了 none,一个可能的选择是使用多线程并将 ReadFile
放在一个单独的线程中,这样它就不会阻塞整个程序,通过使用该方法,我可以在等待读取完成时定期检查进程是否仍然存在...或者 kill/stop 如果进程消失,则线程。
我确实更喜欢解决问题而不是选择解决方法,但我愿意接受任何其他解决方案来使其发挥作用。提前致谢!
编辑:阅读@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(接受答案后):它确实有效!请参阅我对已接受答案的最后评论。
您没有正确管理管道,或者更具体地说,您没有控制管道句柄的继承。 DO NOT 让子进程继承你管道的读句柄(output_pipes[0]
),否则当子进程结束时管道不会正确中断。
阅读 MSDN 了解更多详情:
Creating a Child Process with Redirected Input and Output
使用SetHandleInformation()
或PROC_THREAD_ATTRIBUTE_LIST
来防止CreateProcess()
将output_pipes[0]
作为可继承句柄传递给子进程。子进程不需要访问该句柄,因此无论如何都不需要将其传递到进程边界。它只需要访问管道的写句柄 (output_pipes[1]
)。
对于匿名管道,读进程和写进程都会有hRead和hWrite的handler,每个进程都有自己的handler(继承后复制)。因此,在您的 child 进程退出并关闭其中的处理程序后,另一个 hWrite 仍在 parent 进程中。一定要注意在写过程中关闭hRead,在读过程中关闭hWrite。
我可以重现这个 ReadFile
问题,如果在设置 child 的 hStdOutput
和 hStdError
后关闭写入处理程序,ReadFile
将 return 0 在 child 进程退出后。
这是我的代码示例,
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.cpp:
#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;
}
我正在开发我的库,它需要在运行时捕获和处理子进程的标准输出(和错误)。当 ReadFile 用于读取输出时出现问题,它不会 return 一旦进程结束(被杀死或退出)。
看起来 ReadFile
无法检测到管道的另一端(写句柄)已关闭。根据文档,它应该 return FALSE
并将最后一个错误设置为 ERROR_BROKEN_PIPE
:
If an anonymous pipe is being used and the write handle has been closed, when ReadFile attempts to read using the pipe's corresponding read handle, the function returns FALSE and GetLastError returns 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] = '[=11=]';
return instance->buffer.data;
}
allium_start
为输出重定向创建管道(它对 stdout 和 stderr 使用相同的管道来获取合并流),然后创建子进程。另一个 allium_read_stdout_line
函数负责读取管道的输出并在遇到新行时 return 对其进行处理。
问题出现在 ReadFile
函数调用中,如果进程退出后没有任何可读取的内容,它永远不会 returns,根据我的理解,进程的所有句柄都被 Windows 当它结束时,所以看起来 ReadFile
无法检测到另一端的管道(写句柄)已关闭的事实。
我该如何解决这个问题?我一直在寻找解决方案,但到目前为止我已经找到了 none,一个可能的选择是使用多线程并将 ReadFile
放在一个单独的线程中,这样它就不会阻塞整个程序,通过使用该方法,我可以在等待读取完成时定期检查进程是否仍然存在...或者 kill/stop 如果进程消失,则线程。
我确实更喜欢解决问题而不是选择解决方法,但我愿意接受任何其他解决方案来使其发挥作用。提前致谢!
编辑:阅读@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(接受答案后):它确实有效!请参阅我对已接受答案的最后评论。
您没有正确管理管道,或者更具体地说,您没有控制管道句柄的继承。 DO NOT 让子进程继承你管道的读句柄(output_pipes[0]
),否则当子进程结束时管道不会正确中断。
阅读 MSDN 了解更多详情:
Creating a Child Process with Redirected Input and Output
使用SetHandleInformation()
或PROC_THREAD_ATTRIBUTE_LIST
来防止CreateProcess()
将output_pipes[0]
作为可继承句柄传递给子进程。子进程不需要访问该句柄,因此无论如何都不需要将其传递到进程边界。它只需要访问管道的写句柄 (output_pipes[1]
)。
对于匿名管道,读进程和写进程都会有hRead和hWrite的handler,每个进程都有自己的handler(继承后复制)。因此,在您的 child 进程退出并关闭其中的处理程序后,另一个 hWrite 仍在 parent 进程中。一定要注意在写过程中关闭hRead,在读过程中关闭hWrite。
我可以重现这个 ReadFile
问题,如果在设置 child 的 hStdOutput
和 hStdError
后关闭写入处理程序,ReadFile
将 return 0 在 child 进程退出后。
这是我的代码示例, 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.cpp:
#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;
}