VCpp父进程交互式操作子进程标准输入输出

⌚Time: 2023-10-02 01:04:38

👨‍💻Author: Jack Ge

父进程接管子进程的标准输入输出和错误,实现对子进程的交互操作。比如子进程是一个类似mysql这种可以交互的命令,执行操作后输出结果,父进程根据结果分析决定执行下一步的命令,从而替代人工的输入。

通过父进程创建子进程,使用管道重定向子进程的输入输出错误可以实现

在 Windows 中,可以使用匿名管道(Anonymous Pipes)实现两个进程之间的通信。匿名管道是一种用于进程间通信的轻量级通信机制,仅适用于父子进程之间或兄弟进程之间的通信。

实现管道发送和接收的具体流程如下:

1.在父进程中创建匿名管道,获取管道的读取端和写入端的句柄。


HANDLE hReadPipe; // 读取端句柄

HANDLE hWritePipe; // 写入端句柄



SECURITY_ATTRIBUTES sa; // 安全属性

sa.nLength = sizeof(SECURITY_ATTRIBUTES);

sa.bInheritHandle = TRUE; // 句柄可以被子进程继承

sa.lpSecurityDescriptor = NULL; // 安全描述符



BOOL bSuccess = CreatePipe(&hReadPipe, &hWritePipe, &sa, 0); // 创建匿名管道

if (!bSuccess) {

    // 管道创建失败,处理错误

    return;

}

2.在父进程中启动子进程,并将管道的写入端句柄传递给子进程的 STARTUPINFO 结构体中。


STARTUPINFO si;//程序启动信息,使用 CreateProcess 函数创建一个新的进程时,指定新进程应该如何被创建和运行

PROCESS_INFORMATION pi;//用于接收 CreateProcess 函数创建的新进程的相关信息,例如其进程句柄、线程句柄和进程 ID 等

memset(&si, 0, sizeof(si));

GetStartupInfo(&si);//获取当前进程的启动信息

si.cb = sizeof(si);//将 si 结构体中的 cb 成员设置为结构体的大小

si.dwFlags = STARTF_USESHOWWINDOW //STARTF_USESHOWWINDOW 是一个标志,告诉 CreateProcess 函数使用 si 结构体中的 wShowWindow 成员来控制新进程的主窗口是否可见。

    | STARTF_USESTDHANDLES;//STARTF_USESTDHANDLES 是一个标志,告诉 CreateProcess 函数使用 si 结构体中的 hStdInput、hStdOutput 和 hStdError 成员来指定新进程的标准输入、标准输出和标准错误句柄

si.wShowWindow = SW_HIDE;//隐藏窗口

si.hStdOutput = hWritePipe;// 将管道写入端句柄传递给子进程标准输出

si.hStdError = hWritePipe;// 将管道写入端句柄传递给子进程标准错误

    

WCHAR cmd[] = L"child.exe";

if (!CreateProcess(NULL,cmd,NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi))//启动子进程

{

    return 0;

}

3.在子进程中使用标准输出输出数据到管道。


printf("Hello, parent process!"); // 输出数据到管道

4.在父进程中使用管道的读取端句柄读取从子进程发送的数据。


char buf[1024]; // 读取缓冲区



DWORD dwReadBytes; // 实际读取的字节数

bSuccess = ReadFile(hReadPipe, buf, sizeof(buf), &dwReadBytes, NULL); // 从管道读取数据

if (!bSuccess) {

    // 读取失败,处理错误

    return;

}



buf[dwReadBytes] = '\0'; // 将读取的数据结尾置0,以便于输出到屏幕

printf("Received string: %s\n", buf);

5.父进程关闭对象句柄


CloseHandle(pi.hProcess);

CloseHandle(pi.hThread);

CloseHandle(hReadPipe);

CloseHandle(hWritePipe);

注意

在 Windows 操作系统中,匿名管道是一种单向通信机制,数据只能从管道的写入端流入,然后从读取端流出,不会在管道内部进行循环传输。

上面的过程是父进程通过获取子进程标准输出的情况,如果需要对子进程发送信息,需要父进程重新打开子进程,建立父进程写入子进程标准输入的管道。这样会导致子进程的关闭,无法进行连续的操作

如果要实现双向通信,可以使用两个单向管道来实现。一个管道用于父进程向子进程发送数据,另一个管道用于子进程向父进程发送数据。这样就可以实现在父子进程之间进行双向通信了。

建立两个管道


    SECURITY_ATTRIBUTES sa;

    HANDLE hChildRead,hParentWrite,hChildWrite,hParentRead;



    sa.nLength = sizeof(SECURITY_ATTRIBUTES);

    sa.bInheritHandle = TRUE;

    sa.lpSecurityDescriptor = NULL;



    if (!CreatePipe(&hChildRead,&hParentWrite,&sa,0))//第一条管道,子进程读取端父进程写入端

    {

        return 0;

    }

    if (!CreatePipe(&hParentRead,&hChildWrite,&sa,0))//第二条管道,父进程读取端子进程写入端

    {

        return 0;

    }

绑定子进程的标准输入输出到管道输入端、输出端的句柄


    si.hStdError = hChildWrite;

    si.hStdOutput = hChildWrite;

    si.hStdInput = hChildRead;

在写入子进程时,使用hParentWrite管道写入端


WriteFile(hParentWrite,buffer,strlen(buffer),&dwWrite,NULL);

读取子进程数据,使用hParentRead管道读取端


ReadFile(hParentRead,buffer,1024,&dwRead,NULL);

为了测试,写了一个小程序x.exe,把接收到的输入加前缀后输出


#include <iostream>

#include <fstream>

#include <string.h>

using namespace std;

int main(){

    while(1){

        char a[1024];

        //fflush(stdin);

        cin>>a;

        cout<<"child recv:"<<a<<endl;

    }

    return 0;

}

父程序


#include <stdio.h>

#include <stdlib.h>

#include <memory.h>

#include <windows.h>

#include <iostream>

using namespace std;



int main(void) {



    

    SECURITY_ATTRIBUTES sa;

    HANDLE hChildRead,hParentWrite,hChildWrite,hParentRead;



    sa.nLength = sizeof(SECURITY_ATTRIBUTES);

    sa.bInheritHandle = TRUE;

    sa.lpSecurityDescriptor = NULL;

    //创建两个管道

    if (!CreatePipe(&hChildRead,&hParentWrite,&sa,0))

    {

        return 0;

    }

    if (!CreatePipe(&hParentRead,&hChildWrite,&sa,0))

    {

        return 0;

    }



    STARTUPINFO si;

    PROCESS_INFORMATION pi;

    ZeroMemory(&si,sizeof(STARTUPINFO));

    GetStartupInfo(&si);

    si.cb = sizeof(STARTUPINFO);

    si.hStdError = hChildWrite;//绑定子进程标准错误到管道写端

    si.hStdOutput = hChildWrite;//绑定子进程标准输出到管道写端

    si.hStdInput = hChildRead;//绑定子进程标准输入到管道读端

    si.wShowWindow = SW_HIDE;//隐藏窗口

    si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;

    //创建子进程

    WCHAR cmd[] = L"x.exe";

    if (!CreateProcess(NULL,cmd,NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi))

    {

        return 0;

    }

    

    DWORD dwRead,dwWrite;

    char buffer[1024];

    //循环写入和输出

    while(1){

        memset(buffer,0,1024);

        cin.getline(buffer,1024);



        sprintf(buffer,"%s\r\n",buffer);//加入\r\n



        WriteFile(hParentWrite,buffer,strlen(buffer),&dwWrite,NULL);//写入管道数据

        printf("%d write\n",dwWrite);//输出写入长度



        while(1){//循环读取管道数据



            DWORD dwAvailable;

            PeekNamedPipe(hParentRead,buffer,1024,NULL,&dwAvailable,NULL);//预览管道,如果没有数据则跳出循环

            if (dwAvailable<=0)

            {

                puts("check no data in pipe");

                break;

            }

            

            puts("check data in pipe");

            ReadFile(hParentRead,buffer,1024,&dwRead,NULL);//读取管道数据

            puts(buffer);//打印数据

        }



    }



    //关闭句柄释放资源

    CloseHandle(pi.hProcess);

    CloseHandle(pi.hThread);

    CloseHandle(hParentWrite);

    CloseHandle(hParentRead);

    CloseHandle(hChildWrite);

    CloseHandle(hChildRead);



    return 0;

}


其中向子进程管道写入数据,要加换行,对于windows换行符是\r\n而不是\n,如果不加换行,子进程的cin不会响应结束。如果只是加入\n,子进程输入缓冲区会有残留,导致第一次cin输入后每次cin都跳过,不能正常输入输出。除非子进程每次输入之前加入


fflush(stdin);

清空缓冲区。

但是对于已有的程序是无法改变的。所以要在末尾加入\r\n

另外,peekNamedPipe 函数将数据从命名管道或匿名管道复制到缓冲区中,而不将其从管道中删除。 它还返回有关管道中的数据的信息。通过它预览管道里面是否有数据,如果没有数据就跳出循环,否则没有数据会一直卡在ReadFile不会返回。

运行程序输出,实现了与子进程的交互输入和输出


fgfg

6 write

check data in pipe

child recv:fgfg



check no data in pipe

dsfsfer

9 write

check data in pipe

child recv:dsfsfer



check no data in pipe

cvbcvbcs222

13 write

check data in pipe

child recv:cvbcvbcs222



check no data in pipe

除此之外,c++的popen也可以实现单向的管道功能。