我司主要业务都是跟工厂合作的,有时需要在自家软件中执行客户给的程序去检查设备,并根据返回值做响应处理,这中间会用到父子进程之间利用匿名管道通信的功能。
这类代码多看几遍就明白了,其实我们平时也经常创建进程并且去执行,最典型的就是cmd命令行格式,哪怕只是运行了一个"ipconfig"语句,或者输入"notepad"就能创建一个新的文本出来,这些都是创建进程的体现。
回归主题,有时客户会给我们单独的exe文件,需要在我们的软件中创建cmd进程去执行它并获得返回值,一般的做法如下:
1.创建匿名管道与数据读写端;
2.创建子进程去执行客户的exe文件,并将子进程的输出端重定向到匿名管道的写数据端,再从匿名管道的读数据端去接收返回值。
那么在此之前,要知道什么是匿名管道和创建进程的注意点;
1. 匿名管道
第一次接触这个概念可能会有些陌生,但写过几遍代码再查查资料就能明白它的作用了;匿名管道就是只有两个口子的容器,只能从一端往容器写入数据,在容器另一端接收数据,用图形表示大概如下:
创建匿名管道API:
BOOL WINAPI CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
参数说明:
PHANDLE hReadPipe
匿名管道的读数据端句柄;
PHANDLE hWritePipe
匿名管道的写数据端句柄;
LPSECURITY_ATTRIBUTES lpPipeAttributes
安全属性的结构体信息;
DWORD nSize
匿名管道数据流的大小,设置为0则使用系统的指定的默认大小;
2. 创建进程
创建进程API:
BOOL WINAPI CreateProcess(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
参数说明:
LPCSTR lpApplicationName
子进程需要执行的文件名,是一个const char/wchar_t *的常量字符串指针;这个一般设置为NULL,因为在第二个参数里需要把完成的cmd执行路径和文件名写清楚,就包含了这个功能。
LPSTR lpCommandLine
cmd执行文件路径与文件名,是char/wchar_t *类型的字符串;如果在指定路径下找不到这个可执行文件,进程就会创建失败。值得注意的是,这里传进去的参数一定要是可写的字符串变量!变量!变量!不能是字符串常量,虽然这个API最后不会改变传进去的字符串,但是在中间会对这个字符串进行一些写操作并还原,所以传字符串常量进去一定会创建进程失败!这个设计感觉有点自相矛盾。
比如下面的写法1可以创建进程成功,但是写法2传进去的是字符串常量,就会创建进程失败:
/*写法1; 正确*/
TCHAR cmdLine[200] = _T("ipconfig");
CreateProcess(NULL, cmdLine, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi);
/*写法2; 错误*/
CreateProcess(NULL, _T("ipconfig"), NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi);
LPSECURITY_ATTRIBUTES lpProcessAttributes
子进程的进程属性结构体信息指针;会指出子进程句柄是否能被其创建的子进程继承,一般用不上,设置为NULL,即不能被继承。
LPSECURITY_ATTRIBUTES lpThreadAttributes
子进程的主线程属性结构体信息指针;会指出子进程的主线程句柄是否能被其创建的子进程继承,一般用不上,设置为NULL,即不能被继承。
BOOL bInheritHandles
设置当前主进程是否能被创建的子进程继承所有句柄,这个设置为TRUE,即可以被继承。
DWORD dwCreationFlags
设置子进程执行的优先级,一般设为NULL,立马执行。
LPVOID lpEnvironment
设置子进程的运行环境,一般设为NULL,即与主进程一样。
LPCSTR lpCurrentDirectory
设置子进程的执行路径,一般设置为NULL,与主进程保持一致。
LPSTARTUPINFOA lpStartupInfo
进程的启动信息结构体指针;这个参数很重要,在这里会设置进程的显示窗口、数据输出流向等信息。
LPPROCESS_INFORMATION lpProcessInformation
进程的句柄信息结构体指针;这个结构体里包含了进程的进程句柄、主线程句柄、进程ID、主线程ID。
下面的代码就是创建一个cmd窗口执行"ipconfig"命令进程并获取返回值的案例:
BOOL ExecuteCmdProcess()
{
TCHAR cmdLine[200] = _T("ipconfig");
TCHAR strBuffer[1024] = {0};
CString strData;
STARTUPINFO si = {0}; /*子进程的启动信息,包含了窗口显示和输出管道等重要信息*/
PROCESS_INFORMATION pi = {0}; /*子进程的结构体信息*/
SECURITY_ATTRIBUTES sa; /*匿名管道的属性设置*/
HANDLE hRead; /*匿名管道的读取句柄*/
HANDLE hWrite; /*匿名管道的写数据句柄*/
/*匿名管道的安全信息结构体设置*/
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
/*创建匿名管道,并指定读写端口句柄*/
if (!CreatePipe(&hRead, &hWrite, &sa, 0))
{
AfxMessageBox("Create pipe fail!");
CloseHandle(hRead);
CloseHandle(hWrite);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return FALSE;
}
/*子进程的属性设置*/
ZeroMemory(&si,sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
GetStartupInfo(&si);
si.wShowWindow = FALSE; /*子进程不单独创建新的窗口*/
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.hStdError = hWrite;
si.hStdOutput = hWrite; /*子进程输出的内容,重定向到匿名管道的写数据端*/
/*创建子进程*/
if (!CreateProcess(NULL, cmdLine, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi))
{
AfxMessageBox(_T("Create process fail!"));
CloseHandle(hRead);
CloseHandle(hWrite);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return FALSE;
}
/*在读取匿名管道里的数据之前,提前关闭写端*/
CloseHandle(hWrite);
/*在匿名管道的读取端,把数据输入到字符串里,注意字节长度一定要够!*/
while (1)
{
if (ReadFile(hRead, strBuffer, 1024, NULL, NULL) == NULL)
{
break;
}
strData += strBuffer;
}
AfxMessageBox(strData);
CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return TRUE;
}
运行结果如下:
最后,这种父子进程通过匿名管道通信的代码,基本都有两个坑:
-
就是之前在CreateProcess的参数说明里的第二个参数,这个必须要传可写的字符串变量进去,不然100%会报错;
-
在最后从匿名管道读取数据的时候,可以看到提前关闭了hWrite(匿名管道写端句柄),因为ReadFile一般是当成阻塞函数来使用的,它从匿名管道读端获得数据期间,如果写端的句柄没有关闭,会被当成是一直在输入数据,从而把ReadFile函数给卡死了,致使程序堵塞。