您的位置:首页 > 其它

利用匿名管道技术实现本地进程通信

2015-09-14 22:37 169 查看
转载自进程间通信,内容在自己实践后的略作改变补充。

本篇博文介绍的这个通过匿名管道来实现进程之间的通信

第一:匿名管道只能实现本地进程之间的通信,不能实现跨网络之间的进程间的通信。

第二:匿名管道只能实现父进程和子进程之间的通信,而不能实现任意两个本地进程之间的通信。

匿名管道概述

既然是匿名管道的话,自然,就是没有名字的管道了,还有一种管道呢,叫做命名管道,

命名管道的功能是很强大的,匿名管道在命名管道面前,功能那是简陋的不行的,

至于命名管道的话,会留到下一篇博文中介绍的,

匿名管道正因为提供的功能很单一,所以它所需要的系统的开销也就比命名管道小很多,

在本地机器上可以使用匿名管道来实现父进程和子进程之间的通信,

这里需要注意两点,第一就是在本地机器上,这是因为匿名管道不支持跨网络之间的两个进程之间的通信,

第二就是实现的是父进程和子进程之间的通信,而不是任意的两个进程。

然后得话还顺便介绍匿名管道的另外一种功能,其通过匿名管道可以实现子进程输出的重定向,

何为输出重定向呢?还请听下面详解:

比如我现在建立一个 Win32 的 Console 程序,然后在其中使用如下代码来输出一些信息:

#include <iostream>
using namespace std;

int main(int argc, char * argv)
{
cout<<"Zachary  XiaoZhen "<<endl<<endl;
cout<<"Happy  New   Year"<<endl<<endl;

system("pause");
}


那么在默认下,编译运行上面的代码时,Windows 会弹出一个黑框框,并且在这个黑框框中显示一些信息,





为什么一定要将输出的信息显示在这个黑框框中呢?有没有办法让其显示在我们自己定义的文本框中呢?而后我们再看一幅截图:



上面画了很多红线的这个区域中的信息来自那里呢?为什么会在这个文本框中输出呢?

其实这就可以通过匿名管道来实现,

在卸载 QQ 游戏这幅截图中呢,其实运行了两个进程,

一个就是我们看到的这个输出了图形界面的进程,我们称之为卸载表象进程(父进程),

而另外一个用来执行真正意义上的卸载的进程我们称之为卸载实质进程(子进程)。

其实该卸载表象进程在其执行过程中创建了卸载实质进程来执行真正的卸载操作,

而后,卸载实质进程会输出上面用红色矩形标记的区域中的信息,

如果我们使用默认的输出的话,卸载实质进程会将上面红色区域标记中的信息输出到默认的黑框框中,

但是我们可以使用匿名管道来更改卸载实质进程的输出,

让其将输出数据输入到匿名管道中,而后卸载表象进程从匿名管道中读取到这些输出数据,

然后再将这些数据显示到卸载表象进程的文本框中就可以了。

而上面的这种用来更改卸载实质进程的输出的技术就称之为输出重定向。

当然与之相对的还有输入重定向的。

我们可以让一个进程的输入来自于匿名管道,而不是我们在黑框框中输入数据。

话说到这份上呢,也可以点出一点东东了,

上面的这个重定向不就是利用匿名管道实现的父进程和子进程之间的通信嘛。

匿名管道的使用

匿名管道主要用于本地父进程和子进程之间的通信,

在父进程中的话,首先是要创建一个匿名管道,

在创建匿名管道成功后,可以获取到对这个匿名管道的读写句柄,

然后父进程就可以向这个匿名管道中写入数据和读取数据了,

但是如果要实现的是父子进程通信的话,那么还必须在父进程中创建一个子进程,

同时,这个子进程必须能够继承和使用父进程的一些公开的句柄,

为什么呢?

因为在子进程中必须要使用父进程创建的匿名管道的读写句柄,

通过这个匿名管道才能实现父子进程的通信,所以必须继承父进程的公开句柄。

同时在创建子进程的时候,

必须将子进程的标准输入句柄设置为父进程中创建匿名管道时得到的读管道句柄,

将子进程的标准输出句柄设置为父进程中创建匿名管道时得到的写管道句柄。

然后在子进程就可以读写匿名管道了。

匿名管道的创建

BOOL WINAPI CreatePipe(
__out   PHANDLE hReadPipe,
__out   PHANDLE hWritePipe,
__in    LPSECURITY_ATTRIBUTES lpPipeAttributes,
__in    DWORD nSize );


参数 hReadPipe 为输出参数,该句柄代表管道的读取句柄。

参数 hWritePipe 为输出参数,该句柄代表管道的写入句柄。

参数 lpPipeAttributes 为一个输入参数,指向一个SECURITY_ATTRIBUTES的结构体指针,

其检测返回的句柄是否能够被子进程继承,如果此参数为 NULL ,则表明句柄不能被继承,

在匿名管道中,由于匿名管道要在父子进程之间进行通信,

而子进程如果想要获得匿名管道的读写句柄,则其只能通过从父进程继承获得,

当一个子进程从其父进程处继承了匿名管道的读写句柄以后,

子进程和父进程之间就可以通过这个匿名管道的读写句柄进行通信了。

所以在这里必须构建一个 SECURITY_ATTRIBUTES 的结构体,

并且该结构体的第三个结构成员变量 bInheritHandle 参数必须设置为 TRUE


从而让子进程可以继承父进程所创建的匿名管道的读写句柄。

typedef struct _SECURITY_ATTRIBUTES {

DWORD nLength;

LPVOID lpSecurityDescriptor;

BOOL bInheritHandle;

} SECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

参数 nSize 用来指定缓冲区的大小,

如果此参数设置为 0 ,则表明系统将使用默认的缓冲区大小。一般将该参数设置为 0 即可。



子进程的创建

BOOL  CreateProcess(
LPCWSTR pszImageName,  LPCWSTR pszCmdLine,
LPSECURITY_ATTRIBUTES psaProcess,
LPSECURITY_ATTRIBUTES psaThread,
BOOL fInheritHandles,  DWORD fdwCreate,
LPVOID pvEnvironment,  LPWSTR pszCurDir,
LPSTARTUPINFOW psiStartInfo,
LPPROCESS_INFORMATION pProcInfo );


参数 pszImageName 是一个指向
NULL
终止的字符串,用来指定可执行程序的名称。

参数 pszCmdLine 用来指定传递给新进程的命令行字符串,一般做法是在pszImageName中传递可执行文件的名称,

pszCmdLine 中传递命令行参数。

参数 psaProcess 即代表当
CreateProcess
函数创建进程时,需要给进程对象设置一个安全性。

参数 psaThread 代表当 CreateProcess函数创建新进程后,需要给该进程的主线程对象设置一个安全性。

参数 fInheritHandles 用来指定父进程随后创建的子进程是否能够继承父进程的对象句柄,

如果该参数设置为 TRUE ,则父进程的每一个可继承的打开句柄都将被子进程所继承,

继承的句柄与原始的句柄拥有同样的访问权。

在匿名管道的使用中,因为子进程需要使用父进程中创建的匿名管道的读写句柄,

所以应该将这个参数设置为 TRUE ,从而可以让子进程继承父进程创建的匿名管道的读写句柄。

参数 fdwCreate 用来指定控件优先级类和进程创建的附加标记。

如果只是为了启动子进程,则并不需要设置它创建的标记,可以将此参数设置为 0

对于这个参数的具体取值列表可以参考 MSDN 。

参数 pvEnvironment 代表指向环境块的指针,

如果该参数设置为 NULL ,则默认将使用父进程的环境。通常给该参数传递 NULL

参数 pszCurDir 用来指定子进程当前的路径,

这个字符串必须是一个完整的路径名,其包括驱动器的标识符,

如果此参数设置为 NULL ,那么新的子进程将与父进程拥有相同的驱动器和目录。

参数 psiStartInfo 指向一个
StartUpInfo
的结构体的指针,用来指定新进程的主窗口如何显示。

typedef struct _STARTUPINFOA {

DWORD cb;

LPSTR lpReserved;

LPSTR lpDesktop;

LPSTR lpTitle;

DWORD dwX;

DWORD dwY;

DWORD dwXSize;

DWORD dwYSize;

DWORD dwXCountChars;

DWORD dwYCountChars;

DWORD dwFillAttribute;

DWORD dwFlags;

WORD wShowWindow;

WORD cbReserved2;

LPBYTE lpReserved2;

HANDLE hStdInput;

HANDLE hStdOutput;

HANDLE hStdError;

} STARTUPINFOA, *LPSTARTUPINFOA;


对于 dwFlags 参数来说,如果其设置为
STARTF_USESTDHANDLES


则将会使用该 STARTUPINFO 结构体中的 hStdInput
hStdOutput
hStdError 成员,

来设置新创建的进程的标准输入,标准输出,标准错误句柄。

参数 pProcInfo 为一个输出参数,

指向一个 PROCESS_INFORMATION 结构体的指针,用来接收关于新进程的标识信息。

typedef struct _PROCESS_INFORMATION
{
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;

}PROCESS_INFORMATION;

其中 hProcess hThread 分别用来标识新创建的进程句柄和新创建的进程的主线程句柄。

dwProcessId dwThreadId 分别是全局进程标识符和全局线程标识符。

前者可以用来标识一个进程,后者用来标识一个线程。

示例:匿名管道实现父子进程间通信

此处本应该是作者的实例程序,不过复制粘贴太麻烦,所以就用自己写的控制台程序代替一下。

//这是对两个结构体的初始化
void setSecurityAttributes(LPSECURITY_ATTRIBUTES pSa)
{
pSa->bInheritHandle = TRUE;	//设置子类能够继承父类句柄
pSa->lpSecurityDescriptor = NULL;
pSa->nLength = sizeof(SECURITY_ATTRIBUTES);
}

void setStartUpInfo(LPSTARTUPINFO lpStartUpInfo)
{
memset(lpStartUpInfo, 0, sizeof(STARTUPINFO));

lpStartUpInfo->cb = sizeof(STARTUPINFO);
lpStartUpInfo->dwFlags = STARTF_USESTDHANDLES;

//设置子进程的标准输入句柄为父进程管道的读数据句柄
lpStartUpInfo->hStdInput = hPipeRead;

//设置子进程的标准输出句柄为父进程管道的写数据句柄
lpStartUpInfo->hStdOutput = hPipeWrite;

//设置子进程的标准错误处理句柄和父进程的标准错误处理句柄一致
lpStartUpInfo->hStdError = GetStdHandle(STD_ERROR_HANDLE);
}


//创建匿名管道
bool createNoNamedPipe()
{
SECURITY_ATTRIBUTES sa;

setSecurityAttributes(&sa);
if(CreatePipe(&hPipeRead, &hPipeWrite, &sa, 0)) return true;
else{
cout<<"创建匿名管道失败!"<<endl;
return false;
}
}


//创建子进程
bool createSubProcess(string program, char *cmdParameter)
{
STARTUPINFO startUpInfo;

setStartUpInfo(&startUpInfo);
if(CreateProcess(program.c_str(), cmdParameter, NULL, NULL, TRUE,
CREATE_NEW_CONSOLE, NULL, NULL, &startUpInfo, &processInfo)) return true;
else{
CloseHandle(hPipeRead);
CloseHandle(hPipeWrite);

hPipeRead = NULL;
hPipeWrite = NULL;
cout<<"创建子进程失败!"<<endl;
return false;
}
}


//读写管道的函数
DWORD parentWritePipe(string str)
{
DWORD dwWrite;

if(WriteFile(hPipeWrite, str.c_str(),
str.length(), &dwWrite, NULL))
{
cout<<"“"<<str<<"”发送成功,共"<<dwWrite<<"个字节。"<<endl;
return dwWrite;
}else{
cout<<"“"<<str<<"”发送失败!"<<endl;
return -1;
}

}

DWORD parentReadPipe()
{
DWORD dwRead;
char *pReadBuf;
string tmp;

pReadBuf = new char[MAX];
//cout<<sizeof(pReadBuf)<<endl;	//结果为4
fill(pReadBuf, pReadBuf+MAX, 0);	// '\0'的对应的十进制数为0

//读取的字节数一定要小于数组大小,以保证数组中至少有一个字符串结束符
if(ReadFile(hPipeRead, pReadBuf, MAX-1, &dwRead, NULL))
{
tmp = pReadBuf;
cout<<tmp;	//输出读取的内容
content += tmp;
return dwRead;
}else{
cout<<"读取失败!"<<endl;
return -1;
}
}


结束语

从上面的效果展示中就可以看出我们确实是实现了父子进程之间通过匿名管道来进行通信,

最后再来总结一下对于匿名管道的使用,

匿名管道一般用于实现本地父子进程之间的通信,其不能实现跨网络进程之间的通信,

同时其也一般只用于实现父进程和子进程之间的通信。

像匿名管道的话,其和邮槽不同,

其可以实现父进程即可以向子进程发送数据,同时父进程又可以从子进程接收到数据。

而且子进程可以接收来自父进程的数据,并且也可以给父进程发送数据。

补充两个可能会用到的函数

BOOL WINAPI GetExitCodeProcess(
__in HANDLE hProcess,
__out LPDWORD lpExitCode
);
这个函数由于判断子进程执行状态,如果lpExitCode返回STILL_ACTIVE,则表明子进程仍在执行,否则子进程已退出。

BOOL WINAPI PeekNamedPipe(
__in HANDLE hNamedPipe, //管道句柄
__out_opt LPVOID lpBuffer, //读取输出缓冲区,可选
__in DWORD nBufferSize, //缓冲区大小
__out_opt LPDWORD lpBytesRead, //接收从管道中读取数据的变量的指针,可选
__out_opt LPDWORD lpTotalBytesAvail, //接收从管道读取的字节总数
__out_opt LPDWORD lpBytesLeftThisMessage
);

这个函数可以用来判断管道中是否有数据可读,即可由于匿名管道,又可用于命名管道。根据lpBytesRead是否为0,可判断管道中是否存在数据,使用该函数读取数据后,管道中的数据仍然存在,不会减少。

顺便说一句,通过CreateProcess创建的程序可以通过cout将内容写入管道,但cin却不能从管道中读取内容,而getchar却可以从管道中读取内容。以上是实验结论,并不明白其中的道理,希望路过的大神能够点拨一下!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: