子进程(通过 CreateProcess)在 getch() 上停顿,重定向标准输出和标准输入

Child process (via CreateProcess) stalls on getch() with redirected stdout and stdin

我正在尝试使用 CreateProcess() 启动一个进程,并将标准输入和标准输出重定向到管道。当子进程仅包含 printf() 语句时,我看到它们通过管道传输到父进程并显示得很好。如果我的子进程执行 printf() 和 _getch() 语句,那么事情就会失败。我考虑了几种可能的 deadlock between the pipes 无济于事:

我怀疑某处存在微妙的配置问题。这是一个较大程序中问题的一部分,但我已将其简化为这个简单的测试用例。为此,我从 "Creating a Child Process with Redirected Input and Output". That worked, so maybe the child process using ReadFile() works, but my problem is _getch() (among other programs that seem to have related failures). I replaced the child process with my test program and it stalls. I try solving deadlocks as above, with the overlapped I/O achieved following this example for using named pipes 的 Microsoft 示例开始(在我的阅读中有人提到命名管道和匿名管道的 Windows 实现相当统一)。

同样,如果子进程仅发出 printfs 但 _getch() 失败,则工作正常。值得注意的是,如果 _getch() 出现在子程序中,那么即使 printfs 也不会出现——即使是在 _getch() 之前发出的 printfs()。我读过管道有缓冲,并且如上所述,它们在管道的另一端等待着潜在的死锁,但我想不出除了下面所做的之外我还能做些什么来避免这种情况。

以防万一,我还确保我有一个大的堆缓冲区用于 command-line buffer since CreateProcess() is known to modify it

这是我的父测试代码,其中第一个布尔值配置 overlapped/not 重叠行为:

#include <string>
#include <Windows.h>
#include <tchar.h>
#include <stdio.h> 
#include <strsafe.h>
#include <conio.h>
#include <assert.h>

TCHAR szCmdline[] = TEXT("child.exe");
bool OverlappedStdOutRd = true;
bool OverlappedStdInWr = true;

#define BUFSIZE 4096 

HANDLE g_hChildStd_IN_Rd = NULL;
HANDLE g_hChildStd_IN_Wr = NULL;
HANDLE g_hChildStd_OUT_Rd = NULL;
HANDLE g_hChildStd_OUT_Wr = NULL;

using namespace std;

void ErrorExit(PTSTR lpszFunction)
// Format a readable error message, display a message box, 
// and exit from the application.
{
    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    DWORD dw = GetLastError();

    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        dw,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&lpMsgBuf,
        0, NULL);

    lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT,
        (lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR));
    StringCchPrintf((LPTSTR)lpDisplayBuf,
        LocalSize(lpDisplayBuf) / sizeof(TCHAR),
        TEXT("%s failed with error %d: %s"),
        lpszFunction, dw, lpMsgBuf);
    MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);

    LocalFree(lpMsgBuf);
    LocalFree(lpDisplayBuf);
    ExitProcess(1);
}

static ULONG PipeSerialNumber = 1;
static BOOL APIENTRY MyCreatePipeEx(
    OUT LPHANDLE lpReadPipe,
    OUT LPHANDLE lpWritePipe,
    IN LPSECURITY_ATTRIBUTES lpPipeAttributes,
    IN DWORD nSize,
    DWORD dwReadMode,
    DWORD dwWriteMode
)
/*++

Routine Description:

The CreatePipeEx API is used to create an anonymous pipe I/O device.
Unlike CreatePipe FILE_FLAG_OVERLAPPED may be specified for one or
both handles.
Two handles to the device are created.  One handle is opened for
reading and the other is opened for writing.  These handles may be
used in subsequent calls to ReadFile and WriteFile to transmit data
through the pipe.

Arguments:

lpReadPipe - Returns a handle to the read side of the pipe.  Data
may be read from the pipe by specifying this handle value in a
subsequent call to ReadFile.

lpWritePipe - Returns a handle to the write side of the pipe.  Data
may be written to the pipe by specifying this handle value in a
subsequent call to WriteFile.

lpPipeAttributes - An optional parameter that may be used to specify
the attributes of the new pipe.  If the parameter is not
specified, then the pipe is created without a security
descriptor, and the resulting handles are not inherited on
process creation.  Otherwise, the optional security attributes
are used on the pipe, and the inherit handles flag effects both
pipe handles.

nSize - Supplies the requested buffer size for the pipe.  This is
only a suggestion and is used by the operating system to
calculate an appropriate buffering mechanism.  A value of zero
indicates that the system is to choose the default buffering
scheme.

Return Value:

TRUE - The operation was successful.

FALSE/NULL - The operation failed. Extended error status is available
using GetLastError.

--*/
{
    HANDLE ReadPipeHandle, WritePipeHandle;
    DWORD dwError;
    CHAR PipeNameBuffer[MAX_PATH];

    //
    // Only one valid OpenMode flag - FILE_FLAG_OVERLAPPED
    //
    if ((dwReadMode | dwWriteMode) & (~FILE_FLAG_OVERLAPPED)) {
        SetLastError(ERROR_INVALID_PARAMETER);
        return FALSE;
    }

    //
    //  Set the default timeout to 120 seconds
    //

    if (nSize == 0) {
        nSize = 4096;
    }

    sprintf_s(PipeNameBuffer,
        "\\.\Pipe\TruthPipe.%08x.%08x",
        GetCurrentProcessId(),
        PipeSerialNumber++      // TODO: Should use InterlockedIncrement() here to be thread-safe.
    );

    ReadPipeHandle = CreateNamedPipeA(
        PipeNameBuffer,
        PIPE_ACCESS_INBOUND | dwReadMode,
        PIPE_TYPE_BYTE | PIPE_WAIT,
        1,              // Number of pipes
        nSize,          // Out buffer size
        nSize,          // In buffer size
        1000,           // Timeout in ms
        lpPipeAttributes
    );

    if (!ReadPipeHandle) {
        return FALSE;
    }

    WritePipeHandle = CreateFileA(
        PipeNameBuffer,
        GENERIC_WRITE,
        0,                         // No sharing
        lpPipeAttributes,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL | dwWriteMode,
        NULL                       // Template file
    );

    if (INVALID_HANDLE_VALUE == WritePipeHandle) {
        dwError = GetLastError();
        CloseHandle(ReadPipeHandle);
        SetLastError(dwError);
        return FALSE;
    }

    *lpReadPipe = ReadPipeHandle;
    *lpWritePipe = WritePipeHandle;
    return(TRUE);
}

bool OutstandingWrite = false;
OVERLAPPED WriteOverlapped;
CHAR chWriteBuf[BUFSIZE];
DWORD dwBytesWritten;
DWORD dwBytesToWrite;

bool OutstandingRead = false;
OVERLAPPED ReadOverlapped;
CHAR chReadBuf[BUFSIZE];
DWORD dwBytesRead;

void OnReadComplete();
void StartOverlappedRead();

void WaitForIO(bool Wait)
{
    HANDLE hEvents[2];
    int iEvent = 0;
    int iReadEvent = -1;
    int iWriteEvent = -1;
    if (OutstandingRead) {
        hEvents[iEvent] = ReadOverlapped.hEvent; 
        iReadEvent = iEvent;
        iEvent++;
    }
    if (OutstandingWrite) {
        hEvents[iEvent] = WriteOverlapped.hEvent; 
        iWriteEvent = iEvent;
        iEvent++;
    }

    DWORD dwStatus = WaitForMultipleObjects(iEvent, hEvents, FALSE, Wait ? INFINITE : 250 /*ms*/);
    int Index = -2;
    switch (dwStatus)
    {
    case WAIT_OBJECT_0: Index = 0; break;
    case WAIT_OBJECT_0 + 1: Index = 1; break;
    case WAIT_TIMEOUT: return;
    default:
        ErrorExit(TEXT("WaitForMultipleObjects"));
    }

    if (Index == iReadEvent)    
    {
        if (!GetOverlappedResult(
            g_hChildStd_OUT_Rd, // handle to pipe 
            &ReadOverlapped, // OVERLAPPED structure 
            &dwBytesRead,            // bytes transferred 
            FALSE))            // do not wait 
            ErrorExit(TEXT("GetOverlappedResult"));

        OutstandingRead = false;
        if (dwBytesRead > 0) OnReadComplete();
        StartOverlappedRead();
    }
    else if (Index == iWriteEvent)
    {
        if (!GetOverlappedResult(
            g_hChildStd_IN_Wr, // handle to pipe 
            &WriteOverlapped, // OVERLAPPED structure 
            &dwBytesWritten,            // bytes transferred 
            FALSE))            // do not wait 
            ErrorExit(TEXT("GetOverlappedResult"));

        if (dwBytesWritten != dwBytesToWrite) ErrorExit(TEXT("Write incomplete."));
        OutstandingWrite = false;
    }
    else ErrorExit(TEXT("WaitForMultipleObjects indexing"));
}

void WriteToPipe(string text)
{   
    BOOL bSuccess = FALSE;

    printf("Writing: %s\n", text.c_str());

    if (!OverlappedStdInWr)
    {
        bSuccess = WriteFile(g_hChildStd_IN_Wr, text.c_str(), (DWORD)text.length(), &dwBytesWritten, NULL);
        if (!bSuccess) ErrorExit(TEXT("WriteToPipe"));
        return;
    }
    else
    {
        while (OutstandingWrite) WaitForIO(true);       // Can only have one outstanding write at a time.

        WriteOverlapped.Offset = 0;
        WriteOverlapped.OffsetHigh = 0;
        WriteOverlapped.Pointer = nullptr;

        if (text.length() > BUFSIZE) ErrorExit(TEXT("Attempt to write too long a message!"));
        CopyMemory(chWriteBuf, text.c_str(), text.length());
        dwBytesToWrite = text.length();

        bSuccess = WriteFile(g_hChildStd_IN_Wr, chWriteBuf, dwBytesToWrite, &dwBytesWritten, &WriteOverlapped);
        if (bSuccess) return;
        if (!bSuccess)
        {
            if (GetLastError() == ERROR_IO_PENDING) {
                OutstandingWrite = true;
                return;
            }
            ErrorExit(TEXT("WriteToPipe"));
        }
    }
}

void OnReadComplete()
{
    chReadBuf[dwBytesRead] = '[=10=]';
    printf("Rx: ");
    for (DWORD ii = 0; ii < dwBytesRead; ii++)
    {
        if (chReadBuf[ii] >= 0x20 && chReadBuf[ii] <= 0x7e) printf("%c", chReadBuf[ii]);
        else
        {
            printf("\0x%02X", chReadBuf[ii]);
        }
        if (chReadBuf[ii] == '\n') printf("\n");
    }
    printf("\n");
}

void StartOverlappedRead()
{
    int loops = 0;
    for (;; loops++)
    {
        if (loops > 10) ErrorExit(TEXT("Read stuck in loop"));

        assert(!OutstandingRead);
        ReadOverlapped.Offset = 0;
        ReadOverlapped.OffsetHigh = 0;
        ReadOverlapped.Pointer = nullptr;

        BOOL Success = ReadFile(g_hChildStd_OUT_Rd, chReadBuf, BUFSIZE - 1, &dwBytesRead, &ReadOverlapped);
        if (!Success && GetLastError() != ERROR_IO_PENDING)
            ErrorExit(TEXT("ReadFile"));
        if (Success)
        {
            if (dwBytesRead > 0)
                OnReadComplete();
            continue;
        }
        else {
            OutstandingRead = true; return;
        }
    }
}

void ReadFromPipe(void)
// Read output from the child process's pipe for STDOUT
// and write to the parent process's pipe for STDOUT. 
// Stop when there is no more data. 
{

    BOOL bSuccess = FALSE;

    if (!OverlappedStdOutRd)
    {       
        for (;;)
        {
            DWORD total_available_bytes;
            if (FALSE == PeekNamedPipe(g_hChildStd_OUT_Rd,
                0,
                0,
                0,
                &total_available_bytes,
                0))
            {
                ErrorExit(TEXT("ReadFromPipe - peek"));
                return;
            }
            else if (total_available_bytes == 0)
            {
                // printf("No new pipe data to read at this time.\n");
                return;
            }

            bSuccess = ReadFile(g_hChildStd_OUT_Rd, chReadBuf, BUFSIZE - 1, &dwBytesRead, NULL);
            if (!bSuccess) ErrorExit(TEXT("ReadFromPipe"));
            if (dwBytesRead == 0) return;

            OnReadComplete();
        }
    }
    else
    {
        if (!OutstandingRead) StartOverlappedRead();        

        WaitForIO(false);       
    }
}

void Create()
{
    SECURITY_ATTRIBUTES saAttr;

    printf("\n->Start of parent execution.\n");

    // Set the bInheritHandle flag so pipe handles are inherited. 

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

    if (!OverlappedStdOutRd)
    {
        // As per the MS example, create anonymous pipes

        // Create a pipe for the child process's STDOUT. 

        if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0))
            ErrorExit(TEXT("StdoutRd CreatePipe"));
    }
    else
    {   
        // Create overlapped I/O pipes (only one side is overlapped).
        if (!MyCreatePipeEx(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0))
            ErrorExit(TEXT("Stdout MyCreatePipeEx"));

        ZeroMemory(&ReadOverlapped, sizeof(ReadOverlapped));        
        ReadOverlapped.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);    // Manual-reset event, unnamed, initially signalled.        
        if (ReadOverlapped.hEvent == NULL)
            ErrorExit(TEXT("CreateEvent Read"));
    }

    // Ensure the read handle to the pipe for STDOUT is not inherited.

    if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0))
        ErrorExit(TEXT("Stdout SetHandleInformation"));

    if (!OverlappedStdInWr)
    {
        // Create a pipe for the child process's STDIN. 

        if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0))
            ErrorExit(TEXT("Stdin CreatePipe"));
    }
    else
    {
        // Create overlapped I/O pipes (only one side is overlapped).
        if (!MyCreatePipeEx(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0, 0, FILE_FLAG_OVERLAPPED))
            ErrorExit(TEXT("Stdin MyCreatePipeEx"));

        ZeroMemory(&WriteOverlapped, sizeof(WriteOverlapped));
        WriteOverlapped.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);   // Manual-reset event, unnamed, initially signalled.        
        if (WriteOverlapped.hEvent == NULL)
            ErrorExit(TEXT("CreateEvent Write"));
    }

    // Ensure the write handle to the pipe for STDIN is not inherited. 

    if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0))
        ErrorExit(TEXT("Stdin SetHandleInformation"));

    // Create the child process. 

    TCHAR* szMutableCmdline = new TCHAR[1024];  
    ZeroMemory(szMutableCmdline, 1024 * sizeof(TCHAR));
    CopyMemory(szMutableCmdline, szCmdline, _tcslen(szCmdline) * sizeof(TCHAR));
    PROCESS_INFORMATION piProcInfo;
    STARTUPINFO siStartInfo;
    BOOL bSuccess = FALSE;

    // Set up members of the PROCESS_INFORMATION structure. 

    ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION));

    // Set up members of the STARTUPINFO structure. 
    // This structure specifies the STDIN and STDOUT handles for redirection.

    ZeroMemory(&siStartInfo, sizeof(STARTUPINFO));
    siStartInfo.cb = sizeof(STARTUPINFO);
    siStartInfo.hStdError = g_hChildStd_OUT_Wr;
    siStartInfo.hStdOutput = g_hChildStd_OUT_Wr;
    siStartInfo.hStdInput = g_hChildStd_IN_Rd;
    siStartInfo.dwFlags |= STARTF_USESTDHANDLES;

    // Create the child process. 

    bSuccess = CreateProcess(NULL,
        szMutableCmdline,     // command line 
        NULL,          // process security attributes 
        NULL,          // primary thread security attributes 
        TRUE,          // handles are inherited 
        0,             // creation flags 
        NULL,          // use parent's environment 
        NULL,          // use parent's current directory 
        &siStartInfo,  // STARTUPINFO pointer 
        &piProcInfo);  // receives PROCESS_INFORMATION 

                       // If an error occurs, exit the application. 
    if (!bSuccess)
        ErrorExit(TEXT("CreateProcess"));
    else
    {
        // Close handles to the child process and its primary thread.
        // Some applications might keep these handles to monitor the status
        // of the child process, for example. 

        CloseHandle(piProcInfo.hProcess);
        CloseHandle(piProcInfo.hThread);
    }
}

int main()
{
    printf("Launching...\n");
    Create();
    Sleep(500);
    ReadFromPipe(); 
    Sleep(250);
    WriteToPipe("A\r\n"); 
    Sleep(250);
    ReadFromPipe();
    WriteToPipe("\r\n"); 
    Sleep(250);
    ReadFromPipe(); 
    WriteToPipe("X\r\n"); 
    Sleep(250);
    ReadFromPipe();
    Sleep(250);
    ReadFromPipe(); 
    printf("Press any key to exit.\n");
    _getch();

    // TODO: Not doing proper cleanup in this test app.  Overlapped I/O, CloseHandles, etc. are outstanding.  Bad.

    return 0;
}

子代码可以很简单:

#include <conio.h>

int main()
{
    printf("Hello!\n");
    _getch();
    printf("Bye!\n");
    return 0;
}

编辑:正如@Rbmm 指出的那样,_getch() 使用ReadConsoleInput()。我假设它使用 CONIN$ 而不是 STDIN。所以问题就变成了:我可以重定向 CONIN$ 还是让父进程写入它?

在 child 过程中 printf 之后可以添加 fflush(stdout);。这将立即将数据从 stdout 缓冲区传输到管道。在某些配置中 stdout 缓冲区数据在行尾字符 \n 处自动刷新,但我不确定在这种情况下是否如此 - 可能不是。

如果您的 child 应该从管道(而不是从控制台)读取数据,请使用 getcharfgetsfreadfscanf 给它们 stdin 作为流参数。

int main()
{
    printf("Hello!\n");
    fflush(stdout);
    getchar();
    printf("Bye!\n");
    fflush(stdout);
    return 0;
}

而且你没有死锁。您的 child 只是在等待来自控制台的字符。按 Enter 键恢复它。