父进程接管子进程的标准输入输出和错误,实现对子进程的交互操作。比如子进程是一个类似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.在子进程中使用标准输出输出数据到管道。
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.父进程关闭对象句柄
注意
在 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;
}
绑定子进程的标准输入输出到管道输入端、输出端的句柄
在写入子进程时,使用hParentWrite管道写入端
读取子进程数据,使用hParentRead管道读取端
为了测试,写了一个小程序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都跳过,不能正常输入输出。除非子进程每次输入之前加入
清空缓冲区。
但是对于已有的程序是无法改变的。所以要在末尾加入\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也可以实现单向的管道功能。