小结:进程通信
剪贴板,是用来实现本地两个进程之间的通信,
邮槽 既可以实现本地两个进程之间的通信,也可以实现本地进程和服务器进程的通信,
当然,上面这两种方式都是有局限性的,比如剪贴板就只能实现本地进程之间的通信,
而在邮槽中,虽然是既可以实现本地进程之间的通信,又能实现本地进程和远程服务器进程的通信,
但是使用邮槽的话,服务端只能接收数据,不能发送数据,而客户端只能发送数据而不能接收数据。
而本篇博文介绍的这个通过匿名管道来实现进程之间的通信的话,局限性就显得更加严重,
第一:匿名管道只能实现本地进程之间的通信,不能实现跨网络之间的进程间的通信。
第二:匿名管道只能实现父进程和子进程之间的通信,而不能实现任意两个本地进程之间的通信。
作为进程通信的一种,在这里也做一个简要的介绍。
匿名管道概述
既然是匿名管道的话,自然,就是没有名字的管道了,还有一种管道呢,叫做命名管道,
命名管道的功能是很强大的,匿名管道在命名管道面前,功能那是简陋的不行的,
至于命名管道的话,前一篇博文中已经做过介绍,
匿名管道正因为提供的功能很单一,所以它所需要的系统的开销也就比命名管道小很多,
在本地机器上可以使用匿名管道来实现父进程和子进程之间的通信,
这里需要注意两点,第一就是在本地机器上,这是因为匿名管道不支持跨网络之间的两个进程之间的通信,
第二就是实现的是父进程和子进程之间的通信,而不是任意的两个进程。
然后得话还顺便介绍匿名管道的另外一种功能,其通过匿名管道可以实现子进程输出的重定向,
何为输出重定向呢?还请听下面详解:
比如我现在建立一个 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 PHANDLEhReadPipe,
__out PHANDLEhWritePipe,
__in LPSECURITY_ATTRIBUTESlpPipeAttributes,
__in DWORDnSize );
参数 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(
LPCWSTRpszImageName, LPCWSTRpszCmdLine,
LPSECURITY_ATTRIBUTESpsaProcess,
LPSECURITY_ATTRIBUTESpsaThread,
BOOLfInheritHandles, DWORDfdwCreate,
LPVOIDpvEnvironment, LPWSTRpszCurDir,
LPSTARTUPINFOWpsiStartInfo,
LPPROCESS_INFORMATIONpProcInfo );
参数 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 分别是全局进程标识符和全局线程标识符。
前者可以用来标识一个进程,后者用来标识一个线程。
示例:匿名管道实现父子进程间通信
父进程实现:(简单 MFC 程序)
项目结构:
消息以及成员函数和成员变量的声明:
public:
//创建匿名管道
afx_msg void OnBnClickedBtnCreatePipe();
//写匿名管道
afx_msg void OnBnClickedBtnWritePipe();
//读匿名管道
afx_msg void OnBnClickedBtnReadPipe();
//定义父进程读匿名管道的成员函数
void ParentReadPipe(void);
//定义父进程写匿名管道的成员函数
void ParentWritePipe(void);
//创建 SECURITY_ATTRIBUTES 结构的成员函数
void CreateSecurityAttributes(PSECURITY_ATTRIBUTES pSa);
//创建 STARTUPINFO 结构的成员函数
void CreateStartUpInfo(LPSTARTUPINFO lpStartUpInfo);
//创建匿名管道的成员函数
void CreateNoNamedPipe(void);
//分别代表要从匿名管道中读的数据和要写到匿名管道中的数据
CString m_CStrReadPipe;
CString m_CStrWritePipe;
//保存创建匿名管道后所得到的对匿名管道的读写句柄
HANDLE hPipeRead;
HANDLE hPipeWrite;
//保证匿名管道只创建一次
BOOL m_PipeIsCreated;
消息映射表定义:
const int dataLength = 100;
CNoNamedPipeParentDlg::CNoNamedPipeParentDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CNoNamedPipeParentDlg::IDD, pParent)
, m_CStrReadPipe(_T(""))
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_PipeIsCreated = FALSE;
}
void CNoNamedPipeParentDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_WRITE_PIPE, m_CStrWritePipe);
DDX_Text(pDX, IDC_EDIT_READ_PIPE, m_CStrReadPipe);
}
BEGIN_MESSAGE_MAP(CNoNamedPipeParentDlg, CDialogEx)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BTN_CREATE_PIPE,
&CNoNamedPipeParentDlg::OnBnClickedBtnCreatePipe)
ON_BN_CLICKED(IDC_BTN_WRITE_PIPE,
&CNoNamedPipeParentDlg::OnBnClickedBtnWritePipe)
ON_BN_CLICKED(IDC_BTN_READ_PIPE,
&CNoNamedPipeParentDlg::OnBnClickedBtnReadPipe)
END_MESSAGE_MAP()
消息处理函数:
//创建匿名管道按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnCreatePipe()
{
if(m_PipeIsCreated == FALSE)
{
this->CreateNoNamedPipe();
}
}
//写入数据到匿名管道中按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnWritePipe()
{
this->ParentWritePipe();
}
//从匿名管道中读取数据按钮的消息处理函数
void CNoNamedPipeParentDlg::OnBnClickedBtnReadPipe()
{
this->ParentReadPipe();
}
//接收数据
void CNoNamedPipeParentDlg::ParentReadPipe(void)
{
DWORD dwRead;
char * pReadBuf;
CString cStrRecvData;
pReadBuf = new char[dataLength];
memset(pReadBuf, 0, dataLength);
if(!ReadFile(hPipeRead, pReadBuf, dataLength, &dwRead, NULL))
{
MessageBox(TEXT(" 从匿名管道接收数据失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
cStrRecvData = " 从匿名管道接收数据成功: ";
cStrRecvData += pReadBuf;
this->m_CStrReadPipe.Empty();
this->m_CStrReadPipe = pReadBuf;
UpdateData(FALSE);
MessageBox(cStrRecvData, TEXT("提示"), MB_ICONINFORMATION);
}
//发送数据
void CNoNamedPipeParentDlg::ParentWritePipe(void)
{
UpdateData();
if(!this->m_CStrWritePipe.IsEmpty())
{
char * pSendData;
DWORD dwWrite;
CString cStrSendData;
//在这里需要将 Unicode 字符集转换为 ASCII 字符集
pSendData = new char[this->m_CStrWritePipe.GetLength() + 1];
memset(pSendData, 0, this->m_CStrWritePipe.GetLength() + 1);
for(int i=0;i<this->m_CStrWritePipe.GetLength();i++)
{
pSendData[i] = (char)this->m_CStrWritePipe.GetAt(i);
}
if(!WriteFile(hPipeWrite, pSendData,
this->m_CStrWritePipe.GetLength() + 1, &dwWrite, NULL))
{
MessageBox(TEXT(" 给匿名管道发送数据失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
cStrSendData = " 给匿名管道发送数据成功: ";
cStrSendData += this->m_CStrWritePipe;
this->m_CStrWritePipe.Empty();
UpdateData(FALSE);
MessageBox(cStrSendData, TEXT("提示"), MB_ICONINFORMATION);
}
else
{
MessageBox(TEXT(" 请先输入要发送给匿名管道的数据 ..."),
TEXT("提示"), MB_ICONERROR);
}
}
//创建 SECURITY_ATTRIBUTES 结构
void CNoNamedPipeParentDlg::CreateSecurityAttributes(PSECURITY_ATTRIBUTES pSa)
{
//这里必须将 bInheritHandle 设置为 TRUE,
//从而使得子进程可以继承父进程创建的匿名管道的句柄
pSa->bInheritHandle = TRUE;
pSa->lpSecurityDescriptor = NULL;
pSa->nLength = sizeof(SECURITY_ATTRIBUTES);
}
//用来初始化新进程的 STARTUPINFO 成员
void CNoNamedPipeParentDlg::CreateStartUpInfo(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);
}
//创建匿名管道
void CNoNamedPipeParentDlg::CreateNoNamedPipe(void)
{
SECURITY_ATTRIBUTES sa;
PROCESS_INFORMATION processInfo;
STARTUPINFO startUpInfo;
CreateSecurityAttributes(&sa);
if(!CreatePipe(&hPipeRead, &hPipeWrite, &sa, 0))
{
MessageBox(TEXT(" 创建匿名管道失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
CreateStartUpInfo(&startUpInfo);
if(!CreateProcess(TEXT("NoNamedPipeChild.exe"),
NULL, NULL, NULL, TRUE,
CREATE_NEW_CONSOLE, NULL, NULL,
&startUpInfo, &processInfo))
{
CloseHandle(hPipeRead);
CloseHandle(hPipeWrite);
hPipeWrite = NULL;
hPipeRead = NULL;
MessageBox(TEXT(" 创建子进程失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
else
{
m_PipeIsCreated = TRUE;
//对于 processInfo.hProcess 和 processInfo.hThread
//这两个句柄不需要使用,所以释放资源
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
}
}
子进程实现:(简单 MFC 程序)
项目结构:
消息以及成员函数和成员变量的声明:
// 实现
protected:
HICON m_hIcon;
// 生成的消息映射函数
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnBnClickedBtnWritePipe();
afx_msg void OnBnClickedBtnReadPipe();
//保存从父进程得到针对于匿名管道的读写句柄
HANDLE hPipeRead;
HANDLE hPipeWrite;
//分别代表要从匿名管道中读的数据和要写到匿名管道中的数据
CString m_CStrWritePipe;
CString m_CStrReadPipe;
//子进程读取匿名管道
void ChildReadPipe(void);
//子进程写匿名管道
void ChildWritePipe(void);
//子进程获取从父进程处继承得到的关于匿名管道的读写句柄
void GetReadWriteHandleFromParent(void);
//只需要获取一次匿名管道的读写句柄即可
BOOL m_IsGettedParentHandle;
消息映射表定义:
const int dataLength = 100;
CNoNamedPipeChildDlg::CNoNamedPipeChildDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(CNoNamedPipeChildDlg::IDD, pParent)
, m_CStrWritePipe(_T(""))
, m_CStrReadPipe(_T(""))
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
this->m_IsGettedParentHandle = FALSE;
}
void CNoNamedPipeChildDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Text(pDX, IDC_EDIT_WRITE_PIPE, m_CStrWritePipe);
DDX_Text(pDX, IDC_EDIT_READ_PIPE, m_CStrReadPipe);
}
BEGIN_MESSAGE_MAP(CNoNamedPipeChildDlg, CDialogEx)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(ID_BTN_WRITE_PIPE,
&CNoNamedPipeChildDlg::OnBnClickedBtnWritePipe)
ON_BN_CLICKED(ID_BTN_READ_PIPE,
&CNoNamedPipeChildDlg::OnBnClickedBtnReadPipe)
END_MESSAGE_MAP()
消息处理函数:
//往匿名管道中写入数据按钮的消息处理函数
void CNoNamedPipeChildDlg::OnBnClickedBtnWritePipe()
{
//如果子进程还没有获得对匿名管道的读写句柄的话需要先获取句柄
this->GetReadWriteHandleFromParent();
ChildWritePipe();
}
//从匿名管道中读取数据按钮的消息处理函数
void CNoNamedPipeChildDlg::OnBnClickedBtnReadPipe()
{
//如果子进程还没有获得对匿名管道的读写句柄的话需要先获取句柄
this->GetReadWriteHandleFromParent();
ChildReadPipe();
}
//从匿名管道读取数据成员函数
void CNoNamedPipeChildDlg::ChildReadPipe(void)
{
DWORD dwRead;
char * pReadBuf;
CString cStrRecvData;
pReadBuf = new char[dataLength];
memset(pReadBuf, 0, dataLength);
//读取数据
if(!ReadFile(hPipeRead, pReadBuf, dataLength, &dwRead, NULL))
{
MessageBox(TEXT(" 从匿名管道接收数据失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
cStrRecvData = " 从匿名管道接收数据成功: ";
cStrRecvData += pReadBuf;
this->m_CStrReadPipe.Empty();
this->m_CStrReadPipe = pReadBuf;
UpdateData(FALSE);
MessageBox(cStrRecvData, TEXT("提示"), MB_ICONINFORMATION);
}
//往匿名管道中写入数据
void CNoNamedPipeChildDlg::ChildWritePipe(void)
{
UpdateData();
if(!this->m_CStrWritePipe.IsEmpty())
{
char * pSendData;
DWORD dwWrite;
CString cStrSendData;
//在这里需要将 Unicode 字符集转换为 ASCII 字符集
pSendData = new char[this->m_CStrWritePipe.GetLength() + 1];
memset(pSendData, 0, this->m_CStrWritePipe.GetLength() + 1);
for(int i=0;i<this->m_CStrWritePipe.GetLength();i++)
{
pSendData[i] = (char)this->m_CStrWritePipe.GetAt(i);
}
//写入数据
if(!WriteFile(hPipeWrite, pSendData,
this->m_CStrWritePipe.GetLength(), &dwWrite, NULL))
{
MessageBox(TEXT(" 给匿名管道发送数据失败 ..."),
TEXT("提示"), MB_ICONERROR);
return;
}
cStrSendData = "给匿名管道发送数据成功: ";
cStrSendData += this->m_CStrWritePipe;
this->m_CStrWritePipe.Empty();
UpdateData(FALSE);
MessageBox(cStrSendData, TEXT("提示"), MB_ICONINFORMATION);
}
else
{
MessageBox(TEXT(" 请先输入要发送给匿名管道的数据 ..."),
TEXT("提示"), MB_ICONERROR);
}
}
//需要获取继承自父进程的匿名管道读写句柄
void CNoNamedPipeChildDlg::GetReadWriteHandleFromParent(void)
{
if(this->m_IsGettedParentHandle == FALSE)
{
hPipeRead = GetStdHandle(STD_INPUT_HANDLE);
hPipeWrite = GetStdHandle(STD_OUTPUT_HANDLE);
this->m_IsGettedParentHandle = TRUE;
}
}
效果展示:
首先需要将子进程的可执行文件拷贝到父进程所在目录下,否则创建进程时会找不到子进程的可执行文件。
启动父进程可执行文件,并单击创建匿名管道按钮,此时会弹出子进程窗口(新建了进程):
再在父进程的左边文本框中输入数据,单击写入数据按钮:
再在子进程窗口中单击读取数据按钮:
再在子进程窗口左边的文本框中输入数据,单击写入数据按钮:
再在父进程窗口中单击读取数据按钮:
结束语
从上面的效果展示中就可以看出我们确实是实现了父子进程之间通过匿名管道来进行通信,
最后再来总结一下对于匿名管道的使用,
匿名管道一般用于实现本地父子进程之间的通信,其不能实现跨网络进程之间的通信,
同时其也一般只用于实现父进程和子进程之间的通信。
像匿名管道的话,其和邮槽不同,
其可以实现父进程即可以向子进程发送数据,同时父进程又可以从子进程接收到数据。
而且子进程可以接收来自父进程的数据,并且也可以给父进程发送数据。
注:以上整理源于网络