进程间通信
进程间通信的概念
每一个进程都是拥有自己独立的虚拟地址空间和页表结构,促使了进程独立,这也导致了进程之间合作存在问题,为了解决该问题,产生了进程间的通信。
Windows操作系统提供了促进应用程序之间的通信和数据共享的机制。
Mailslots
mailslot 是一种机制,用于通过 IPC (单向) 。 应用程序可以将消息存储在 mailslot 中。 mailslot 的所有者可以检索存储在其中的消息。 这些消息通常通过网络发送到指定计算机或指定域中的所有计算机。
一个重要的注意事项是 mailslots 使用数据报广播消息。这也意味着,通信过程中存在信息丢失的可能。
关于Mailslots
- mailslot 是驻留在内存中的伪文件,可以使用标准文件函数来访问它。 与磁盘文件不同,mailslot 是临时的。 关闭 mailslot 的所有句柄后,将删除 mailslot 及其包含的所有数据。
- 计算机之间发送时不能大于 424 字节。
- Mailslots 可以广播域中的消息。 如果域中多个进程使用同一名称创建 mailslot,则参与进程将接收发送到该 mailslot 并发送到域的每条消息。
命名规则
当进程创建 mailslot 时,mailslot 名称必须采用以下格式。
\.\mailslot \ [ 路径 \ ] 名称
Mailslot 名称需要以下元素:两个反斜杠开始名称、句点、句点后的反斜杠、单词 “mailslot” 和尾随反斜杠。 名称不区分大小写。 Mailslot 名称前面可以是由一个或多个目录的名称组成的路径,用反斜杠分隔。 例如,如果用户期望来自 Bob、Pete 和 Sue 的税收的邮件,用户的 mailslot 应用程序可能允许用户为每个发件人创建单独的 mailslots,如下所示:
\.\mailslot \ 税金 \ bobs-sfpreviewcluster _ 注释
\.\mailslot \ 税金 \petes _ 注释
\.\mailslot \ 税金 \ 使用 disk write _ 注释
- 若要将消息放入 mailslot,进程会按名称打开邮件槽。 若要在本地计算机上写入 mailslot,进程可以使用格式与创建 mailslot 相同的 mailslot 名称。 但这种情况相对较为罕见。 更常见的情况是,使用以下形式写入特定远程计算机上的 mailslot:
\ComputerName \mailslot \ [ 路径 \ ] 名称
- 若要将消息放入具有给定名称的指定域中的每个 mailslot,请使用以下形式:
\DomainName \mailslot \ [ 路径 \ ] 名称
- 若要将消息放入系统主域中具有给定名称的每个 mailslot,请使用以下形式:
\*\mailslot \ [ 路径 \ ] 名称
使用
创建 Mailslot
HANDLE CreateMailslotA(
[in] LPCSTR lpName,
[in] DWORD nMaxMessageSize,
[in] DWORD lReadTimeout,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
返回值:HANDLE(void*)句柄
参数:
-
[in]lpName
Mailslot的名称。此名称必须具有以下格式:
\.\Mailslot[path]名称
名称字段必须是唯一的。该名称可能包括由反斜杠分隔的多级伪目录 -
[in]nMaxMessageSize
可以写入邮件槽的单个邮件的最大大小,以字节为单位。要指定消息的大小,将此值设置为零。 -
[in]lReadTimeout
在超时发生之前,读取操作可以等待消息写入邮件槽的时间,以毫秒为单位。以下值具有特殊含义。为0时:如果没有消息,则立即返回;为-1:
永远等待消息。 -
[in,optional]lpSecurityAttributes
指向安全属性结构的指针。结构的bInheritHandle成员确定返回的句柄是否可以被子进程继承。如果lpSecurityAttributes为NULL,则无法继承句柄。
#include <windows.h>
#include <iostream>
bool Create(LPCTSTR name, HANDLE& slot) {
slot = CreateMailslot(name,
0,//默认为大消息
-1,//始终等待消息
NULL);//无法继承句柄
if (slot == INVALID_HANDLE_VALUE) {
return false;
} else {
return true;
}
}
void main()
{
LPCTSTR SlotName = TEXT("\\.\\mailslot\\test_mailslot");
HANDLE slot;
bool ret = Create(SlotName,slot);
if (ret == false) {
printf("create Mailslot faild,%d\n", GetLastError());
} else {
printf("create Mailslot success\n");
}
}
写入 Mailslot
流程:用CreateFile在已经运行的mailslot服务器上创建一个文件,用WriteFile写入内容
写入 mailslot 类似于写入标准磁盘文件。 可使用 CreateFile、WriteFile和 CloseHandle函数在mailslot 中放入消息。 该消息将广播到本地计算机上的 mailslot 服务器。
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile );
返回值:操作句柄
参数:
- [in] lpFileName
要创建或打开的文件或设备的名称。
[in] dwDesiredAccess
请求的对文件或设备的访问权限,可概括为读、写、二者均为0
[in] dwShareMode
文件或设备的请求共享模式,可以是读、写、两者都可以
[in] dwCreationDisposition
对存在或不存在的文件或设备执行的操作。
[in] dwFlagsAndAttributes
文件或设备属性和标志,文件属性是文件最常见的默认值。
此参数可以包括可用文件属性的任意组合(文件属性)。所有其他文件属性将覆盖文件属性
[in, optional] hTemplateFile
具有通用读取权限的模板文件的有效句柄。模板文件为正在创建的文件提供文件属性和扩展属性。
此参数可以为NULL。
打开现有文件时,CreateFile会忽略此参数
BOOL WriteFile(
[in] HANDLE hFile,
[in] LPCVOID lpBuffer,
[in] DWORD nNumberOfBytesToWrite,
[out, optional] LPDWORD lpNumberOfBytesWritten,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
返回值:true false
参数:
-
[in] hFile
文件或I/O设备的句柄 -
[in] lpBuffer
指向包含要写入文件或设备的数据的缓冲区的指针 -
[in] nNumberOfBytesToWrite
要写入文件或设备的字节数。 -
[out, optional] lpNumberOfBytesWritten
指向使用同步hFile参数时接收写入字节数的变量的指针。 -
[in, out, optional] lpOverlapped
如果在打开hFile参数时文件_FLAG_OVERLAPPED,则需要指向重叠结构的指针,否则该参数可以为NULL。
#include <windows.h>
#include <iostream>
bool Create(LPCTSTR name, HANDLE& slot) {
slot = CreateMailslot(name,
0,//默认为大消息
-1,//始终等待消息
NULL);//无法继承句柄
if (slot == INVALID_HANDLE_VALUE) {
return false;
} else {
return true;
}
}
bool Write(HANDLE slot,LPCTSTR message) {
DWORD cb;
bool ret = WriteFile(slot, message, (DWORD)(lstrlen(message) + 1 * sizeof(TCHAR)), &cb, (LPOVERLAPPED)NULL);
if (ret == false) {
return false;
} else {
return true;
}
}
void main()
{
LPCTSTR SlotName = TEXT("\\\\.\\mailslot\\test_mailslot");
HANDLE slot;
bool ret = Create(SlotName,slot);
if (ret == false) {
printf("create Mailslot faild,%d\n", GetLastError());
} else {
printf("create Mailslot success\n");
}
HANDLE hFile;
hFile = CreateFile(SlotName,
GENERIC_WRITE,
FILE_SHARE_READ,
(LPSECURITY_ATTRIBUTES)NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
(HANDLE)NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("CreateFile failed with %d.\n", GetLastError());
}
ret = Write(hFile, TEXT("write for test1"));
if (ret == false) {
printf("write Mailslot faild,%d\n", GetLastError());
} else {
printf("write Mailslot success\n");
}
}
读取Mailslot
BOOL ReadFile(
[in] HANDLE hFile,
[out] LPVOID lpBuffer,
[in] DWORD nNumberOfBytesToRead,
[out, optional] LPDWORD lpNumberOfBytesRead,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
BOOL GetMailslotInfo(
[in] HANDLE hMailslot,
[out, optional] LPDWORD lpMaxMessageSize,
[out, optional] LPDWORD lpNextSize,
[out, optional] LPDWORD lpMessageCount,
[out, optional] LPDWORD lpReadTimeout
);
BOOL ReadSlot()
{
DWORD cbMessage, cMessage, cbRead;
BOOL fResult;
LPTSTR lpszBuffer;
TCHAR achID[80];
DWORD cAllMessages;
cbMessage = cMessage = cbRead = 0;
fResult = GetMailslotInfo(slot, // mailslot handle
(LPDWORD)NULL, // no maximum message size
&cbMessage, // size of next message
&cMessage, // number of messages
(LPDWORD)NULL); // no read time-out
if (!fResult)
{
printf("GetMailslotInfo failed with %d.\n", GetLastError());
return FALSE;
}
if (cbMessage == MAILSLOT_NO_MESSAGE)
{
printf("Waiting for a message...\n");
return TRUE;
}
cAllMessages = cMessage;
while (cMessage != 0) // retrieve all messages
{
// Create a message-number string.
StringCchPrintf((LPTSTR)achID,
80,
TEXT("\nMessage #%d of %d\n"),
cAllMessages - cMessage + 1,
cAllMessages);
// Allocate memory for the message.
lpszBuffer = (LPTSTR)GlobalAlloc(GPTR,
lstrlen((LPTSTR)achID) * sizeof(TCHAR) + cbMessage);
if (NULL == lpszBuffer)
return FALSE;
lpszBuffer[0] = '\0';
fResult = ReadFile(slot,
lpszBuffer,
cbMessage,
&cbRead,
NULL);
if (!fResult)
{
printf("ReadFile failed with %d.\n", GetLastError());
GlobalFree((HGLOBAL)lpszBuffer);
return FALSE;
}
// Concatenate the message and the message-number string.
StringCbCat(lpszBuffer,
lstrlen((LPTSTR)achID) * sizeof(TCHAR) + cbMessage,
(LPTSTR)achID);
// Display the message.
_tprintf(TEXT("Contents of the mailslot: %s\n"), lpszBuffer);
GlobalFree((HGLOBAL)lpszBuffer);
fResult = GetMailslotInfo(slot, // mailslot handle
(LPDWORD)NULL, // no maximum message size
&cbMessage, // size of next message
&cMessage, // number of messages
(LPDWORD)NULL); // no read time-out
if (!fResult)
{
printf("GetMailslotInfo failed (%d)\n", GetLastError());
return FALSE;
}
}
return TRUE;
}
管道
管道 是处理用于通信的共享内存部分。 创建管道的进程是 管道服务器。 连接到管道的进程是 管道客户端。
通信过程简单来说就是:一个进程向管道写入信息,而另一个进程从管道中读取信息。
关于管道
有两种类型的管道: 匿名管道 和 命名管道。 匿名管道比命名管道需要更少的开销,但提供有限的服务。
匿名管道
匿名 管道 是一种未命名的单向管道,在父进程和子进程之间传输数据。 匿名管道始终为本地管道;它们不能用于网络通信。
匿名管道创建
BOOL CreatePipe(
[out] HANDLE* hReadPipe,
[out] HANDLE* hWritePipe,
[in, optional] &SECURITY_ATTRIBUTES lpPipeAttributes,
[in] DWORD nSize
);
返回值:
失败返回0,否则返回非0
参数:
-
[out] hReadPipe
出参,接收管道读取句柄的指针 -
[out] hWritePipe
出参,接收管道写入句柄的指针 -
[in, optional] lpPipeAttributes
指向安全属性结构的指针,该结构确定返回的句柄是否可以由子进程继承。如果lpPipeAttributes为NULL,则无法继承句柄。 如果管道服务器将此结构的 bInheritHandle 成员设置为 TRUE,则可以继承 CreatePipe 创建的句柄。使用SECURITY_ATTRIBUTES来操作较为方便。 -
[in] nSize
管道缓冲区的大小,以字节为单位。大小只是一个建议;系统使用该值来计算适当的缓冲机制。如果此参数为零,系统将使用默认缓冲区大小。
从管道中读取和写入,使用ReadFile和WriteFile即可。如果管道缓冲区已满并且要写入的字节数更多, 则 WriteFile 不会返回,直到另一个进程从管道读取数据,从而提供更多的缓冲区空间。
#include <windows.h>
#include <iostream>
#include <Shlwapi.h>
bool Create(PHANDLE&& write, PHANDLE&& read) {
WORD ret = 0;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = 0;
sa.bInheritHandle = true;
ret = CreatePipe(read, write, &sa, 0);
if (ret == 0) {
return false;
} else {
return true;
}
}
int main()
{
HANDLE write, read;
bool ret = false;
ret = Create(&write, &read);
if (ret == false) {
std::cout << "create pipe faild " << GetLastError() << std::endl;
} else {
std::cout << "create pipe success" << std::endl;
}
return 0;
}
命名管道
命名 管道 是命名单向或双工管道,用于管道服务器与一个或多个管道客户端之间的通信。 命名管道的所有实例共享相同的管道名称,但每个实例都有自己的缓冲区和句柄,并且为客户端/服务器通信提供单独的管道。
任何进程都可以充当服务器和客户端,使对等通信成为可能。
用于实例化命名管道的服务器端函数是 CreateNamedPipe。 用于接受连接的服务器端函数为 ConnectNamedPipe。 客户端进程使用 CreateFile 或 CallNamedPipe 函数连接到命名管道。
命名管道可用于在同一台计算机上的进程之间或网络中不同计算机上的进程之间提供通信。
命名规则
在 CreateFile 、WaitNamedPipe或 CallNamedPipe 函数中指定管道的名称时,请使用以下形式:
\ServerName \pipe \ PipeName
其中 ServerName 是远程计算机的名称或时间段,用于指定本地计算机。 PipeName 指定的管道名称字符串可以包含反杠外的任何字符,包括数字和特殊字符。 整个管道名称字符串的长度最多为 256 个字符。 管道名称不区分大小写。
管道服务器无法在另一台计算机上创建管道,因此 CreateNamedPipe 必须为服务器名称使用一个时间段,如以下示例所示。
\.\pipe \ PipeName
访问模式
对于服务器:
- 如果管道服务器使用 PIPE ACCESS INBOUND 创建管道,则管道服务器为只读管道,管道客户端为只写。
- 如果管道服务器使用 PIPE ACCESS OUTBOUND 创建管道,则管道对于管道服务器为只写,对于管道客户端为只读。
- 使用PIPE ACCESS DUPLEX 创建的管道对于管道服务器和管道客户端都是读/写。
对于客户端:
使用 CreateFile 连接到命名管道 的管道客户端必须在 dwDesiredAccess参数中指定访问权限,该访问权限与管道服务器指定的访问模式兼容。
例如,客户端必须指定 GENERIC READ访问权限,以打开管道服务器使用 PIPE ACCESS OUTBOUND 创建的管道的句柄。
对于管道的所有实例,访问模式必须相同。
相关操作
HANDLE CreateNamedPipeA(
[in] LPCSTR lpName,
[in] DWORD dwOpenMode,
[in] DWORD dwPipeMode,
[in] DWORD nMaxInstances,
[in] DWORD nOutBufferSize,
[in] DWORD nInBufferSize,
[in] DWORD nDefaultTimeOut,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
返回值:
如果函数成功,则返回值是命名管道实例服务器端的句柄。
参数:
-
[in] lpName
唯一的管道名称。此字符串必须具有以下形式:
\.\pipe\pipename -
[in] dwOpenMode
开放模式
PIPE_ACCESS_DUPLEX :管道是双向的;服务器和客户端进程都可以读取和写入管道
PIPE_ACCESS_INBOUND:管道中的数据流仅从客户端流向服务器。
PIPE_ACCESS_OUTBOUND:管道中的数据流仅从服务器流向客户端。 -
[in] dwPipeMode
管道模式。
PIPE_TYPE_BYTE,数据以字节流的形式写入/流出管道。
PIPE_TYPE_MESSAGE:数据作为消息流写入/流出管道。 -
[in] nMaxInstances
可以为此管道创建的最大实例数。如果此参数为PIPE_UNLIMITED_INSTANCES,则可以创建的管道实例数量仅受系统资源可用性的限制。 -
[in] nOutBufferSize
为输出缓冲区保留的字节数。 -
[in] nInBufferSize
为输入缓冲区保留的字节数。 -
[in] nDefaultTimeOut
超时值
如果WaitNamedPipe函数指定NMPWAIT_USE_default_WAIT,则默认超时值 -
[in, optional] lpSecurityAttributes
指向安全属性结构的指针,该结构为新命名管道指定安全描述符,并确定子进程是否可以继承返回的句柄。如果lpSecurityAttributes为NULL,则命名管道将获得默认的安全描述符,并且无法继承句柄。
管道服务器创建管道实例后,管道客户端就可以通过调用 CreateFile来连接管道实例
BOOL WaitNamedPipeA(
[in] LPCSTR lpNamedPipeName,
[in] DWORD nTimeOut
);
返回值:
如果在超时间隔过去之前管道实例可用,则返回值为非零。
参数:
-
[in] lpNamedPipeName
命名管道的名称。 -
[in] nTimeOut
函数等待命名管道的实例可用的毫秒数。
NMPWAIT_USE_DEFAULT_WAIT:超时间隔是服务器进程在CreateNamedPipe函数中指定的默认值
**NMPWAIT_WAIT_FOREVER:**在指定管道的实例可用之前,函数不会返回。
BOOL ConnectNamedPipe(
[in] HANDLE hNamedPipe,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
返回值:
如果操作是同步的,则ConnectNamedPipe在操作完成之前不会返回。如果函数成功,则返回值为非零。如果函数失败,返回值为零。
参数:
-
[in] hNamedPipe
命名管道实例的服务器端句柄。此句柄由CreateNamedPipe函数返回 -
[in, out, optional] lpOverlapped
指向重叠结构的指针。
管道进阶
多线程管道服务器
服务器创建管道,采用多线程,当有连接管道时创建新线程服务
#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <strsafe.h>
#define BUFSIZE 512
DWORD WINAPI InstanceThread(LPVOID);
VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD);
int main()
{
BOOL fConnected = FALSE;
DWORD dwThreadId = 0;
HANDLE hPipe = INVALID_HANDLE_VALUE, hThread = NULL;
LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe");
for (;;)
{
_tprintf(TEXT("\nPipe Server: Main thread awaiting client connection on %s\n"), lpszPipename);
hPipe = CreateNamedPipe(
lpszPipename,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE |
PIPE_READMODE_MESSAGE |
PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFSIZE,
BUFSIZE,
0,
NULL);
if (hPipe == INVALID_HANDLE_VALUE)
{
_tprintf(TEXT("CreateNamedPipe failed, GLE=%d.\n"), GetLastError());
return -1;
}
fConnected = ConnectNamedPipe(hPipe, NULL) ?
TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (fConnected)
{
printf("Client connected, creating a processing thread.\n");
hThread = CreateThread(
NULL,
0,
InstanceThread,
(LPVOID)hPipe,
0,
&dwThreadId);
if (hThread == NULL)
{
_tprintf(TEXT("CreateThread failed, GLE=%d.\n"), GetLastError());
return -1;
} else CloseHandle(hThread);
} else
CloseHandle(hPipe);
}
return 0;
}
DWORD WINAPI InstanceThread(LPVOID lpvParam)
{
HANDLE hHeap = GetProcessHeap();
TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE * sizeof(TCHAR));
TCHAR* pchReply = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE * sizeof(TCHAR));
DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0;
BOOL fSuccess = FALSE;
HANDLE hPipe = NULL;
if (lpvParam == NULL)
{
printf("\nERROR - Pipe Server Failure:\n");
printf(" InstanceThread got an unexpected NULL value in lpvParam.\n");
printf(" InstanceThread exitting.\n");
if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
return (DWORD)-1;
}
if (pchRequest == NULL)
{
printf("\nERROR - Pipe Server Failure:\n");
printf(" InstanceThread got an unexpected NULL heap allocation.\n");
printf(" InstanceThread exitting.\n");
if (pchReply != NULL) HeapFree(hHeap, 0, pchReply);
return (DWORD)-1;
}
if (pchReply == NULL)
{
printf("\nERROR - Pipe Server Failure:\n");
printf(" InstanceThread got an unexpected NULL heap allocation.\n");
printf(" InstanceThread exitting.\n");
if (pchRequest != NULL) HeapFree(hHeap, 0, pchRequest);
return (DWORD)-1;
}
printf("InstanceThread created, receiving and processing messages.\n");
hPipe = (HANDLE)lpvParam;
while (1)
{
fSuccess = ReadFile(
hPipe,
pchRequest,
BUFSIZE * sizeof(TCHAR),
&cbBytesRead,
NULL);
if (!fSuccess || cbBytesRead == 0)
{
if (GetLastError() == ERROR_BROKEN_PIPE)
{
_tprintf(TEXT("InstanceThread: client disconnected.\n"));
} else
{
_tprintf(TEXT("InstanceThread ReadFile failed, GLE=%d.\n"), GetLastError());
}
break;
}
GetAnswerToRequest(pchRequest, pchReply, &cbReplyBytes);
fSuccess = WriteFile(
hPipe,
pchReply,
cbReplyBytes,
&cbWritten,
NULL);
if (!fSuccess || cbReplyBytes != cbWritten)
{
_tprintf(TEXT("InstanceThread WriteFile failed, GLE=%d.\n"), GetLastError());
break;
}
}
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
HeapFree(hHeap, 0, pchRequest);
HeapFree(hHeap, 0, pchReply);
printf("InstanceThread exiting.\n");
return 1;
}
VOID GetAnswerToRequest(LPTSTR pchRequest,
LPTSTR pchReply,
LPDWORD pchBytes)
{
_tprintf(TEXT("Client Request String:\"%s\"\n"), pchRequest);
if (FAILED(StringCchCopy(pchReply, BUFSIZE, TEXT("default answer from server"))))
{
*pchBytes = 0;
pchReply[0] = 0;
printf("StringCchCopy failed, no outgoing message.\n");
return;
}
*pchBytes = (lstrlen(pchReply) + 1) * sizeof(TCHAR);
}