匿名管道概述
匿名管道是一个没有名称的管道,本质上是一块共享的内存区域。匿名管道可以实现在本机上的父子进程之间的通信。这里需要注意两点,第一就是在本地机器上,这是因为匿名管道不支持跨网络之间的两个进程之间的通信,第二就是实现的是父进程和子进程之间的通信,而不是任意的两个进程。匿名管道正是因为提供的功能很单一,所以他所需要的系统开销也就比命名管道小很多。
然后顺便介绍一个匿名管道的另一种功能,其通过匿名管道可以实现子进程输出重定向。
匿名管道的使用
匿名管道主要用于本机父进程和子进程之间的通信,
在父进程中的话,需要首先创建一个匿名管道,在创建匿名管道成功后,可以获取对这个匿名管道的读写句柄,然后父进程就可以向这个匿名管道中写入数据和读取数据,
但是如果要实现父子进程通信的话,那么还需在父进程中创建一个子进程,同时,这个子进程必须继承和使用父进程的一些公开句柄。同时,在创建子进程是,必须将子进程的标准输入句柄设置为父进程创建匿名管道时得到的读管道句柄,将子进程的标准输出句柄设置为父进程创建管道时得到的写管道句柄
匿名管道常用的API
创建匿名管道—CreatePipe
BOOL CreatePipe(
[out] PHANDLE hReadPipe,
[out] PHANDLE hWritePipe,
[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,
[in] DWORD nSize
);
- 参数hReadPipe
指向接收管道读取句柄的变量指针
- 参数hWritePipe
指向接收管道写入句柄的变量指针
- 参数lpPipeAttributes
指向SECURITY_ATTRIBUTES结构的指针,通过该结构体确定函数返回的句柄是否可以被子进程继承,如果为NULL则无法继承,而在匿名管道通信中,必须让子进程继承父进程的句柄
- 参数nSize
缓冲区的大小,零表示系统使用默认的缓冲区大小。
CreatePipe function (namedpipeapi.h) - Win32 apps |微软学习 (microsoft.com)
在父进程中创建一个子进程,调用CreateProcess函数创建一个子进程
创建进程—CreateProcessW
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
CreateProcess函数有TM十个参数,这里就不挨个介绍了,可以参考微软官方文档。
CreateProcessW function (processthreadsapi.h) - Win32 apps |微软学习 (microsoft.com)
最后一个参数lpProcessInformation指向接收有关新进程的表示信息的结构体指针。
我们可以通过这个结构体设定子进程的读写句柄,例如下面这种:
其中hReadPipe和hWritePipe是父进程创建的读写句柄,其他属性的值使用的是标准默认值。
STARTUPINFO strStartupInfo;//用来指定新进程的窗口如何显示
memset(&strStartupInfo, 0, sizeof(strStartupInfo));
strStartupInfo.cb = sizeof(strStartupInfo);
strStartupInfo.dwFlags = STARTF_USESTDHANDLES;
strStartupInfo.hStdInput = hReadPipe;//读句柄
strStartupInfo.hStdOutput = hWritePipe;//写句柄
strStartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//选择一个默认的句柄(标准的错误句柄
读取数据—ReadFile
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
- 参数hFile
设备的句柄
- 参数lpBuffer
指向存储读取数据的缓冲区指针
- 参数nNumberOfBytesToRead
要读取的最大字节数
- 参数lpNumberOfBytesRead
指向变量的指针,该变量接收使用同步hFile参数时读取的字节数。
- 参数lpOverlapped
指向重叠结构的指针是 如果使用FILE_FLAG_OVERLAPPED 打开hFile参数,则为必需,否则可以为NULL。
ReadFile function (fileapi.h) - Win32 apps |微软学习 (microsoft.com)
写数据—WriteFile
BOOL WriteFile(
[in] HANDLE hFile,
[in] LPCVOID lpBuffer,
[in] DWORD nNumberOfBytesToWrite,
[out, optional] LPDWORD lpNumberOfBytesWritten,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
- 参数hFile
设备的句柄
- 参数lpBuffer
指向写入数据的缓冲区指针
- 参数nNumberOfBytesToRead
写入的字节数
- 参数lpNumberOfBytesRead
指向变量的指针,该变量接收使用同步hFile参数时读取的字节数。
- 参数lpOverlapped
指向重叠结构的指针是 如果使用FILE_FLAG_OVERLAPPED 打开hFile参数,则为必需,否则可以为NULL。
Demo示例
创建两个MFC应用,在父进程中添加三个菜单分别为“创建管道”、”读取数据“、”写入数据“,点击创建管道会创建一个匿名管道和下面MFC应用的进程,点击读数据会从匿名管道中读取数据,并通过消息框弹出,点击写数据会向匿名管道写入一段数据
在子进程中添加两个才惨分别为”读数据“、”写数据“。点击读数据会读取匿名管道中的数据,点击写数据会向匿名管道中写入数据。
父进程
将读句柄和写句柄添加到类的属性里面,在构造函数里初始化句柄(赋为空),析构函数里关闭句柄。
创建管道:
void CChildView::OnPipeCreate()
{
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;//是否被继承
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
//创建匿名管道
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
MessageBox(_T("匿名管道创建失败"));
return;
}
//创建子进程
STARTUPINFO strStartupInfo;//用来指定新进程的窗口如何显示
memset(&strStartupInfo, 0, sizeof(strStartupInfo));
strStartupInfo.cb = sizeof(strStartupInfo);
strStartupInfo.dwFlags = STARTF_USESTDHANDLES;
strStartupInfo.hStdInput = hReadPipe;//读句柄
strStartupInfo.hStdOutput = hWritePipe;//写句柄
strStartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);//选择一个默认的句柄(标准的错误句柄
PROCESS_INFORMATION szProcessInformation;
memset(&szProcessInformation, 0, sizeof(szProcessInformation));
int iRet = CreateProcess(
_T("MailslotClient.exe"),
NULL,
NULL,
NULL,
true,
NULL,
NULL,
NULL,
&strStartupInfo,
&szProcessInformation
);
if (iRet) {
//创建成功
//进程创建成功后将返回的句柄关闭,内核计数-1
CloseHandle(szProcessInformation.hProcess);
CloseHandle(szProcessInformation.hThread);
szProcessInformation.dwProcessId = 0;
szProcessInformation.dwThreadId = 0;
szProcessInformation.hThread = NULL;
szProcessInformation.hProcess = NULL;
}
else {
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
hReadPipe = NULL;
hWritePipe = NULL;
MessageBox(_T("创建进程失败"));
return;
}
}
读数据:
void CChildView::OnPipeRead()
{
//匿名管道读
char szBuf[100] = { 0 };
DWORD dwRead;
if (!ReadFile(hReadPipe, szBuf, 100, &dwRead, NULL)) {
MessageBox(_T("读取数据失败"));
CloseHandle(hReadPipe);
return;
}
MessageBox((CString)szBuf);
}
写数据:
void CChildView::OnPipeWrite()
{
//匿名管道写
char szBuf[] = "霸道小明超帅";
DWORD dwWrite;
if (!WriteFile(hWritePipe, szBuf, strlen(szBuf) + 1, &dwWrite, NULL)) {
MessageBox(_T("写入数据失败"));
CloseHandle(hWritePipe);
return;
}
}
子进程
在子进程中同样将读写句柄设为类的属性,但此时就不能在构造函数中初始为NULL了, 因为子进程只会在读写数据的时候使用这两个句柄(作为入参,因此不能初始为空),可以使用GetStdHandle(STD_INPUT_HANDLE)和GetStdHandle(STD_OUTPUT_HANDLE)初始为标准的读写句柄。
CChildView::CChildView()
{
hReadCliPipe = GetStdHandle(STD_INPUT_HANDLE);
hWriteCliPipe = GetStdHandle(STD_OUTPUT_HANDLE);
}
读数据:
void CChildView::OnCliPipeRead()
{
//匿名管道读
char szBuf[100] = { 0 };
DWORD dwRead;
if (!ReadFile(hReadCliPipe, szBuf, 100, &dwRead, NULL)) {
MessageBox(_T("读取数据失败"));
CloseHandle(hReadCliPipe);
return;
}
MessageBox((CString)szBuf);
}
写数据:
void CChildView::OnCliPipeWrite()
{
//匿名管道写
char szBuf[] = "霸道小明超帅";
DWORD dwWrite;
if (!WriteFile(hWriteCliPipe, szBuf, strlen(szBuf) + 1, &dwWrite, NULL)) {
MessageBox(_T("写入数据失败"));
CloseHandle(hWriteCliPipe);
return;
}
}
执行结果: