CreateProcess cmd.exe 读/写管道死锁

Posted

技术标签:

【中文标题】CreateProcess cmd.exe 读/写管道死锁【英文标题】:CreateProcess cmd.exe read/write pipes deadlock 【发布时间】:2017-10-06 17:41:10 【问题描述】:

您好,我正在尝试为 cmd.exe 制作前端 GUI,以便将其扩大,但我卡住了。

我尝试设计这样的 API

char* Directory = WriteCommand("dir");
printf("- %s\n", Directory);

并且输出看起来与 cmd 窗口中的完全一样,除了我将它放在一个字符串中,所以它会是

DATE TIME FILESIZE FILENAME
etc etc etc

然后我可以发出

char* Up = WriteCommand ("cd ..");

它会给我上面的目录列表。所以我想要一个通过使用管道读写的终端控制。

我根据这个 MSDN 示例代码尝试了很多东西 - https://msdn.microsoft.com/en-us/library/ms682499.aspx

但我认为这段代码只适用于发出一个命令并读取一个响应,因为在此处描述的死锁之后 - https://blogs.msdn.microsoft.com/oldnewthing/20110707-00/?p=10223

我在这里看到了其他几个问题,比如这个有类似问题的问题 - How to read output from cmd.exe using CreateProcess() and CreatePipe() 但没有发布适合我的解决方案。

这是我的代码。

#include <windows.h> 
#include <tchar.h>
#include <stdio.h> 
#include <strsafe.h>

#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;

HANDLE g_hInputFile = NULL;

void CreateChildProcess(void);
void WriteToPipe(char* Arg1);
void ReadFromPipe(void);
void ErrorExit(PTSTR);



int _tmain(int argc, TCHAR *argv[])

    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;

    // 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"));

    // 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"));

    // 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"));

    // 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. 

    CreateChildProcess();

    // Get a handle to an input file for the parent. 
    // This example assumes a plain text file and uses string output to verify data flow. 

/*if (argc == 1)
    ErrorExit(TEXT("Please specify an input file.\n"));

g_hInputFile = CreateFile(
    argv[1],
    GENERIC_READ,
    0,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_READONLY,
    NULL);

if (g_hInputFile == INVALID_HANDLE_VALUE)
    ErrorExit(TEXT("CreateFile"));*/

    // Write to the pipe that is the standard input for a child process. 
    // Data is written to the pipe's buffers, so it is not necessary to wait
    // until the child process is running before writing data.



// Read from pipe that is the standard output for child process. 


ReadFromPipe();

WriteToPipe("ipconfig");

// THIS IS WHERE DEADLOCK OCCURS, FROM HERE
// PROGRAM BECOMES UNRESPONSIVE - HOW TO FIX THIS?

ReadFromPipe();



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

// The remaining open handles are cleaned up when this process terminates. 
// To avoid resource leaks in a larger application, close handles explicitly. 

return 0;


void CreateChildProcess()
// Create a child process that uses the previously created pipes for     STDIN and STDOUT.

   TCHAR szCmdline[] = TEXT("cmd.exe /k");
    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,
    "cmd.exe",     // 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);



void WriteToPipe(char* Command)

// Read from a file and write its contents to the pipe for the    child's STDIN.
// Stop when there is no more data. 
   
   DWORD dwRead, dwWritten;
    CHAR chBuf[BUFSIZE];
    BOOL bSuccess = FALSE;

    bSuccess = WriteFile(g_hChildStd_IN_Wr, Command, strlen(Command), &dwWritten, NULL);
    if (bSuccess == FALSE)
        printf("write fail\n");

    printf("written = %i\n", dwWritten);


//for (;;)
//
    //bSuccess = ReadFile(g_hInputFile, chBuf, BUFSIZE, &dwRead, NULL);
    //if (!bSuccess || dwRead == 0) break;

    //bSuccess = WriteFile(g_hChildStd_IN_Wr, Command, strlen(Command), &dwWritten, NULL);
    //if (bSuccess == FALSE)
        //printf("write fail\n");

    //printf("written = %i\n", dwWritten);
//

// Close the pipe handle so the child process stops reading. 

//if (!CloseHandle(g_hChildStd_IN_Wr))
    //ErrorExit(TEXT("StdInWr CloseHandle"));


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. 

DWORD dwRead, dwWritten;
CHAR chBuf[BUFSIZE];
BOOL bSuccess = FALSE;
HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

int i;

for (i = 0; i < 4; i++)


    /*DWORD dwAvail = 0;
    if (!PeekNamedPipe(g_hChildStd_OUT_Rd, NULL, 0, NULL, &dwAvail, NULL)) 
        // error, the child process might have ended
        break;
    
    if (!dwAvail) 
        // no data available in the pipe
        break;
    */

    bSuccess = ReadFile(g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL);
    if (!bSuccess || dwRead == 0) break;

    /*bSuccess = WriteFile(hParentStdOut, chBuf, dwRead, &dwWritten, NULL);
    if (!bSuccess) break;*/

    chBuf[dwRead] = '\0';

    printf("%i - %s\n", i, chBuf);


printf("done\n");

我发出初始的“cmd.exe”命令,它让我开始了命令提示符。我现在想发出“ipconfig”(或任何其他命令)来获取网络信息。程序死锁并变得无响应。我无法再读取子进程的输出。我怎样才能解决这个问题?感谢您的帮助。

【问题讨论】:

如果您为自己的程序创建异步管道句柄并且将使用管道进行异步工作 - 您永远不会遇到死锁 1) 你只需要通过CreateNamedPipe(和FILE_FLAG_OVERLAPPED)+CreateFile创建一个管道对。您在代码中创建 两个 管道对。 2.) 通过BindIoCompletionCallbackCreateThreadpoolIo 将您的管道端绑定到IOCP 3) 调用只需调用ReadFile,然后每次从竞争回调中再次调用ReadFile,当之前的ReadFile 完成时没有错误。 4)很少,如果您需要在普通过程中处理读取结果,而不是在回调中,您可以创建光纤,在读取数据回调中 - 切换到光纤,并在处理数据后切换回 @RbMm:代码正在创建 2 个管道对,因为它正在重定向 STDIN 和 STDOUT。两者不能使用相同的管道。 @RemyLebeau - 你错了。我很多时候用 one 管道对编写代码。这是完美的工作。分配给 STDIN 和 STDOUT 的 same 管道句柄。我们可以在管道上进行读写操作 需要通过CreateNamedPipePIPE_ACCESS_DUPLEX 创建管道对 - 管道将是双向的;服务器和客户端进程都可以读取和写入管道。我说这不是理论上的,但很多时候在代码中实现它 【参考方案1】:

避免任何死锁的最强大和有效的解决方案 - 使用异步 io。永远不要等待 IO (read,write,ioctl) 就位完成,而是在回调中处理它。

还要注意使用管道进行重定向输出 - 非常常见的错误,我们需要为 STDINSTDOUT 使用不同的句柄,并且需要创建 2 个不同的管道对 - 一个用于 STDIN 和另一个用于 STDOUT。这是错误的。我们可以为 STDINSTDOUT(以及 STDERROR)使用 single 管道句柄。

    我们需要使用CreateNamedPipeW 创建服务器管道句柄 PIPE_ACCESS_DUPLEX|FILE_READ_DATA|FILE_WRITE_DATA|FILE_FLAG_OVERLAPPED 标志。通过使用PIPE_ACCESS_DUPLEX,我们创建了双向管道, 结果服务器和客户端进程都可以读写 到管道。和FILE_FLAG_OVERLAPPED 作为异步 模式。我们也没有使这个句柄可继承,所以不需要调用 SetHandleInformation就可以了 我们由CreateFileW 创建的客户端句柄也带有 FILE_GENERIC_READ|FILE_GENERIC_WRITE 访问 - 这给了能力 将它分配给 stdinstdout。因为客户(比如 cmd.exe) 通常假设同步 io - 我们不使用 FILE_FLAG_OVERLAPPED 在这里。也通过使用 lpSecurityAttributes 我们 只需使此句柄可继承即可。 我们需要将服务器句柄绑定到一些 IOCP,以便在 io 时调用回调 结束。这里我们有 3 个变体 - 使用 BindIoCompletionCallback - 最简单的方式还是使用 CreateThreadpoolIo。我们也可以自己创建 IOCP 并拥有 线程池,但是对于重定向子进程输出,这种方式通常 不需要。 创建子进程后 - 我们需要关闭客户端管道句柄 (我们复制给孩子)然后在我们的管道上调用ReadFile 处理。当这个ReadFile 完成时——我们需要再次调用 ReadFile 来自回调等等 - 直到我们没有收到错误 ReadFile 完成(通常是 ERROR_BROKEN_PIPE)。所以我们需要 一直有来自管道的有效读取请求,直到断开连接。 我们可以随时随地免费拨打WriteFile - 这绝不会 导致死锁,因为我们使用异步io。 如果我们需要对读取进行复杂处理,有时(非常罕见) 数据(基于以前的结果和状态),这更容易 在普通程序中处理而不是在回调中处理,我们可以创建光纤 对于这个任务 (CreateFiber) 和工作线程回调, 读取完成后 - 第一次调用 ConvertThreadToFiber(如果我们 为同一个工作线程多次调用它 - 将是错误的 ERROR_ALREADY_FIBER 在第二次和下一次通话中,但这没关系。但 所有这些工作仅从 vista 开始。这里的xp错误)。记住 当前光纤,到需要退休的地方(GetCurrentFiber())和 致电SwitchToFiber(使用我们专用的读取光纤)- 在哪里 我们可以处理读取结果,然后通过调用返回 SwitchToFiber(带有用于工作线程的光纤)。但是这一切 在非常罕见和特定的情况下确实需要。通常 处理所有是与管道句柄相关的对象中具有状态的回调 - 绰绰有余。

简单的 cmd 示例

#define _XP_SUPPORT_

struct IO_COUNT 

    HANDLE _hFile;
    HANDLE _hEvent;
    LONG _dwIoCount;

    IO_COUNT()
    
        _dwIoCount = 1;
        _hEvent = 0;
    

    ~IO_COUNT()
    
        if (_hEvent)
        
            CloseHandle(_hEvent);
        
    

    ULONG Create(HANDLE hFile);

    void BeginIo()
    
        InterlockedIncrement(&_dwIoCount);
    

    void EndIo()
    
        if (!InterlockedDecrement(&_dwIoCount))
        
            SetEvent(_hEvent);
        
    

    void Wait()
    
        WaitForSingleObject(_hEvent, INFINITE);
    
;


struct U_IRP : OVERLAPPED 

    enum  read, write ;

    IO_COUNT* _pIoObject;
    ULONG _code;
    LONG _dwRef;
    char _buffer[256];

    void AddRef()
    
        InterlockedIncrement(&_dwRef);
    

    void Release()
    
        if (!InterlockedDecrement(&_dwRef)) delete this;
    

    U_IRP(IO_COUNT* pIoObject) : _pIoObject(pIoObject)
    
        _dwRef = 1;
        pIoObject->BeginIo();
        RtlZeroMemory(static_cast<OVERLAPPED*>(this), sizeof(OVERLAPPED));
    

    ~U_IRP()
    
        _pIoObject->EndIo();
    

    ULONG CheckIoResult(BOOL fOk)
    
        if (fOk)
        
#ifndef _XP_SUPPORT_
            OnIoComplete(NOERROR, InternalHigh);
#endif
            return NOERROR;
        

        ULONG dwErrorCode = GetLastError();

        if (dwErrorCode != ERROR_IO_PENDING)
        
            OnIoComplete(dwErrorCode, 0);
        

        return dwErrorCode;
    

    ULONG Read()
    
        _code = read;

        AddRef();

        return CheckIoResult(ReadFile(_pIoObject->_hFile, _buffer, sizeof(_buffer), 0, this));
    

    ULONG Write(const void* pvBuffer, ULONG cbBuffer)
    
        _code = write;

        AddRef();

        return CheckIoResult(WriteFile(_pIoObject->_hFile, pvBuffer, cbBuffer, 0, this));
    

    VOID OnIoComplete(DWORD dwErrorCode, DWORD_PTR dwNumberOfBytesTransfered)
    
        switch (_code)
        
        case read:
            if (dwErrorCode == NOERROR)
            
                if (dwNumberOfBytesTransfered)
                
                    if (int cchWideChar = MultiByteToWideChar(CP_OEMCP, 0, _buffer, (ULONG)dwNumberOfBytesTransfered, 0, 0))
                    
                        PWSTR wz = (PWSTR)alloca(cchWideChar * sizeof(WCHAR));

                        if (MultiByteToWideChar(CP_OEMCP, 0, _buffer, (ULONG)dwNumberOfBytesTransfered, wz, cchWideChar))
                        
                            if (int cbMultiByte = WideCharToMultiByte(CP_ACP, 0, wz, cchWideChar, 0, 0, 0, 0))
                            
                                PSTR sz = (PSTR)alloca(cbMultiByte);

                                if (WideCharToMultiByte(CP_ACP, 0, wz, cchWideChar, sz, cbMultiByte, 0, 0))
                                
                                    DbgPrint("%.*s", cbMultiByte, sz);
                                
                            
                        
                    
                
                Read();
            
            break;
        case write:
            break;
        default:
            __debugbreak();
        

        Release();

        if (dwErrorCode)
        
            DbgPrint("[%u]: error=%u\n", _code, dwErrorCode);
        
    

    static VOID WINAPI _OnIoComplete(
        DWORD dwErrorCode,
        DWORD_PTR dwNumberOfBytesTransfered,
        LPOVERLAPPED lpOverlapped
        )
    
        static_cast<U_IRP*>(lpOverlapped)->OnIoComplete(RtlNtStatusToDosError(dwErrorCode), dwNumberOfBytesTransfered);
    
;

ULONG IO_COUNT::Create(HANDLE hFile)

    _hFile = hFile;
    // error in declaration LPOVERLAPPED_COMPLETION_ROUTINE : 
    // second parameter must be DWORD_PTR but not DWORD
    return BindIoCompletionCallback(hFile, (LPOVERLAPPED_COMPLETION_ROUTINE)U_IRP::_OnIoComplete, 0) && 
#ifndef _XP_SUPPORT_
        SetFileCompletionNotificationModes(hFile, FILE_SKIP_COMPLETION_PORT_ON_SUCCESS) &&
#endif
        (_hEvent = CreateEvent(0, TRUE, FALSE, 0)) ? NOERROR : GetLastError();


void ChildTest()

    static const WCHAR name[] = L"\\\\?\\pipe\\somename";

    HANDLE hFile = CreateNamedPipeW(name, 
        PIPE_ACCESS_DUPLEX|FILE_READ_DATA|FILE_WRITE_DATA|FILE_FLAG_OVERLAPPED, 
        PIPE_TYPE_BYTE|PIPE_READMODE_BYTE, 1, 0, 0, 0, 0);

    if (hFile != INVALID_HANDLE_VALUE)
    
        IO_COUNT obj;

        if (obj.Create(hFile) == NOERROR)
        
            BOOL fOk = FALSE;

            SECURITY_ATTRIBUTES sa =  sizeof(sa), 0, TRUE ;

            STARTUPINFOW si =  sizeof(si) ;
            PROCESS_INFORMATION pi;

            si.dwFlags = STARTF_USESTDHANDLES;

            si.hStdError = CreateFileW(name, FILE_GENERIC_READ|FILE_GENERIC_WRITE, 
                FILE_SHARE_READ|FILE_SHARE_WRITE, &sa, OPEN_EXISTING, 0, 0);

            if (si.hStdError != INVALID_HANDLE_VALUE)
            
                si.hStdInput = si.hStdOutput = si.hStdError;

                WCHAR ApplicationName[MAX_PATH];
                if (GetEnvironmentVariableW(L"ComSpec", ApplicationName, RTL_NUMBER_OF(ApplicationName)))
                
                    if (CreateProcessW(ApplicationName, 0, 0, 0, TRUE, 0, 0, 0, &si, &pi))
                    
                        CloseHandle(pi.hThread);
                        CloseHandle(pi.hProcess);
                        fOk = TRUE;
                    
                

                CloseHandle(si.hStdError);
            

            if (fOk)
            
                STATIC_ASTRING(help_and_exit, "help\r\nexit\r\n");

                U_IRP* p;

                if (p = new U_IRP(&obj))
                
                    p->Read();
                    p->Release();
                

                obj.EndIo();

                //++ simulate user commands
                static PCSTR commands[] =  "help\r\n", "ver\r\n", "dir\r\n", "exit\r\n" ;
                ULONG n = RTL_NUMBER_OF(commands);
                PCSTR* psz = commands;
                do 
                
                    if (MessageBoxW(0,0, L"force close ?", MB_YESNO) == IDYES)
                    
                        DisconnectNamedPipe(hFile);
                        break;
                    
                    if (p = new U_IRP(&obj))
                    
                        PCSTR command = *psz++;
                        p->Write(command, (ULONG)strlen(command) * sizeof(CHAR));
                        p->Release();
                        
                 while (--n);
                //--

                obj.Wait();
            
        

        CloseHandle(hFile);
    

【讨论】:

感谢您的代码-您能告诉我您使用的是什么编译器吗?我遇到了很多错误,STATIC_WSTRING 未定义,FILE_SHARE_VALID_FLAGS 未定义,还有其他一些错误。不过感谢您的帮助。 @Ferdinand STATIC_WSTRING - 这是我的宏,我忘了为它显示 #define,它可以非常简单地更改。 #define FILE_SHARE_VALID_FLAGS 0x00000007wdm.h 中定义 是的,我将其更改为 wchar_t* 名称,并且 wdm.h 不包含在 VS2017 中。在我深入探讨之前,您能否确认此代码适用于 XP - 10?另外,什么是 "\\\\?\\pipe\\46728DEE-F87C-4ef8-B2F0-1B5204862FF0" ?为什么我们不使用 cmd.exe? @Ferdinand - 我修复了代码。这个代码变体在 xp 上不起作用,因为我使用 SetFileCompletionNotificationModes - 如果需要 - 我删除这个调用和位更改 CheckIoResult - 之后这也将在 xp 上工作 @Ferdinand - 我更改了代码。您可以使用或不使用#define _XP_SUPPORT_(实际上仅更改了2行)。 "\\\\?\\pipe\\***" - 这只是唯一的管道名称。你还有编译器错误吗? RtlNtStatusToDosError【参考方案2】:

我知道它有点旧,所以你可能不再需要这个答案了。但是对于那些来 *** 寻求相同问题的解决方案的人,我在构建类似项目时遇到了同样的问题,我找到了解决方案。

基本上,只需在命令末尾添加“\n”换行符。这是模拟按下“ENTER”按钮所必需的。否则,WriteFile() 有效,但 ReadFile() 仍在等待,因为该命令从未在子进程 cmd.exe 中执行,因此 ReadFile() 没有任何内容可供读取,导致它挂在那里。

所以修改后的代码是(我没有测试运行下面的代码,只是根据原作者发布的示例修改):

void WriteToPipe(char* Command)

// Read from a file and write its contents to the pipe for the child's STDIN.
// Stop when there is no more data. 
   
   DWORD dwRead, dwWritten;
    CHAR chBuf[BUFSIZE];
    BOOL bSuccess = FALSE;

    // Fix for the issue
    strcat_s(command, strlen(command) + 1, "\n", 1);

    bSuccess = WriteFile(g_hChildStd_IN_Wr, Command, strlen(Command), &dwWritten, NULL);
    if (bSuccess == FALSE)
        printf("write fail\n");

    printf("written = %i\n", dwWritten);


//for (;;)
//
    //bSuccess = ReadFile(g_hInputFile, chBuf, BUFSIZE, &dwRead, NULL);
    //if (!bSuccess || dwRead == 0) break;

    //bSuccess = WriteFile(g_hChildStd_IN_Wr, Command, strlen(Command), &dwWritten, NULL);
    //if (bSuccess == FALSE)
        //printf("write fail\n");

    //printf("written = %i\n", dwWritten);
//

// Close the pipe handle so the child process stops reading. 

//if (!CloseHandle(g_hChildStd_IN_Wr))
    //ErrorExit(TEXT("StdInWr CloseHandle"));

【讨论】:

以上是关于CreateProcess cmd.exe 读/写管道死锁的主要内容,如果未能解决你的问题,请参考以下文章

为啥 CreateProcess 中的 cmd.exe 的行为与 DOS 提示符不同?

CreateProcess 调用 cmd.exe 包括。没有显示(闪烁)窗口的参数?

CreateProcess() API 函数有啥用?

无法识别 C++ CreateProcess 'telnet'

C语言 -- CreateProcess创建进程

使用 createProcess() 运行批处理文件