进程通信
所谓进程通信
,其本质就是共享内存
一,邮槽
邮槽(Mailslot)是一种简单的 进程间通信(IPC)机制,它允许不同进程之间传递信息。邮槽的设计比较简单,适用于 单向通信 的场景,也就是说,一个进程只能向邮槽写入数据,另一个进程才能从中读取数据。它是 Windows 操作系统提供的一个特性。
邮槽的工作原理:
- 服务端:创建一个邮槽(Mailslot),用于接收客户端的消息。服务端可以是一个持续运行的程序,它会持续等待其他进程向它的邮槽写入数据。
- 客户端:客户端打开已经存在的邮槽,并向邮槽写入消息。客户端只能写入数据,无法读取数据。
特点:
- 单向通信:邮槽只允许单向通信,即客户端只能向邮槽写数据,服务端只能从邮槽读取数据。
- 广播通信:邮槽支持广播通信,一个进程可以向多个接收者发送消息。如果多个客户端监听同一个邮槽,它们会收到相同的消息。
- 简易性:与其他更复杂的 IPC 机制(如共享内存、管道等)相比,邮槽的实现简单,适用于需要快速且简单实现的场景。
- 无连接:邮槽是无连接的通信机制,客户端和服务端不需要事先建立连接,服务端只需要创建一个邮槽,客户端可以随时通过路径打开它。
使用场景:
邮槽适合用于简单的单向消息传递,例如:
- 客户端向服务器发送一个通知或请求。
- 服务端向多个客户端广播某个事件。
它在一些简单的应用程序或旧版 Windows 系统中可能仍然使用,但在现代应用程序中,可能会选择更灵活和高效的通信机制,如 管道 或 网络套接字。
邮槽的创建与使用:
- 创建邮槽:服务端创建一个邮槽,指定它的路径。
- 使用
CreateMailslot
函数来创建邮槽。
- 写入数据:客户端向邮槽写入数据。
- 使用
CreateFile
打开邮槽,并使用 WriteFile
向其写入数据。
- 读取数据:服务端从邮槽中读取数据。
示例:
服务端代码如下:Process1.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <iostream> #include <Windows.h>
int main() { CHAR szBuffer[MAX_PATH] = { 0 };
HANDLE hMailsolt = CreateMailslot(L"\\\\.\\mailslot\\TesT", 0, MAILSLOT_WAIT_FOREVER, NULL);
DWORD dwRead; if(!ReadFile(hMailsolt, szBuffer,MAX_PATH,&dwRead,NULL)) { CloseHandle(hMailsolt); } printf("%s\r\n", szBuffer); CloseHandle(hMailsolt); system("pause"); return 0; }
|
客户端代码如下:Process2.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> #include <Windows.h> int main() { CHAR szBuffer[MAX_PATH] = "Hello World!";
DWORD dwRet; HANDLE h = CreateFile(L"\\\\.\\mailslot\\TesT", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); WriteFile(h, szBuffer, strlen(szBuffer) + 1, &dwRet, NULL); CloseHandle(h); system("pause"); return 0;
}
|
最后成功向邮槽中写入:Hello world!
如图:
等待写入消息:process1.cpp
成功向邮槽中写入内容:如下:
二,管道
管道(Pipe) 是一种常见的进程间通信(IPC)机制,允许不同进程之间通过共享内存区域进行数据交换。管道可以看作是一个“数据通道”,进程可以通过它将数据传递给另一个进程。
管道的类型:
- 匿名管道(Anonymous Pipe):
- 只能在父子进程之间使用,即一个父进程和它的子进程之间传递数据。
- 通常用于在同一台机器上进行进程间的简单通信。
- 数据传输是单向的,但可以通过创建两个管道(一个用于从父进程到子进程,另一个用于反向通信)来实现双向通信。
- 命名管道(Named Pipe):
- 可以在任意进程之间进行通信,不限于父子进程,可以是不同计算机上的进程。
- 它通过一个名字在系统中创建,允许不同的进程通过该名字打开管道进行数据交换。
- 命名管道支持双向通信。
管道的工作原理:
- 写入端(写入数据):一个进程将数据写入管道的写入端。
- 读取端(读取数据):另一个进程从管道的读取端读取数据。
匿名管道:
匿名管道通常在父子进程之间使用,可以在创建管道时指定管道的读端和写端。匿名管道适合于同一台计算机上进程间的简单数据传输。
命名管道:
命名管道在系统中有一个名字,可以被任何进程通过该名字访问,因此它不仅可以用于本地进程之间的通信,还可以用于跨网络进行通信。命名管道可以支持双向通信,即允许数据在两个方向上流动。
管道的特点:
- 单向或双向通信:匿名管道通常是单向的(写入端和读取端),而命名管道可以支持双向通信。
- 进程间通信:管道是为了进程间的通信设计的,可以在父子进程间,或者是跨进程(甚至跨计算机)进行数据交换。
- 阻塞性:默认情况下,管道是阻塞的。如果管道的缓冲区满了,写操作将被阻塞;如果管道为空,读取操作会被阻塞。
管道的创建与使用:
- 创建管道:在 Windows 中,匿名管道使用
CreatePipe
函数创建;命名管道使用 CreateNamedPipe
函数创建。
- 读写管道:进程可以通过
ReadFile
和 WriteFile
来进行管道的读写操作。
示例:
下面两个进程是不同进程:
process1.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| #include <iostream> #include <Windows.h>
int main() { HANDLE hPipe = CreateNamedPipe(L"\\\\.\\pipe\\test", PIPE_ACCESS_DUPLEX| FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE, 1, 1024, 1024, 0, NULL );
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
OVERLAPPED lvLap; ZeroMemory(&lvLap, sizeof(lvLap)); lvLap.hEvent = hEvent; ConnectNamedPipe(hPipe, &lvLap
); WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);
char szBuffer[0x100] = { 0 }; DWORD dwSize = 0;
ReadFile(hPipe,szBuffer,0x100,&dwSize,NULL); printf("%s\r\n", szBuffer);
char WriteBuffer[] = "Hello"; DWORD dwRetSize = 0; WriteFile(hPipe, WriteBuffer, strlen(WriteBuffer)+1, &dwRetSize, NULL);
system("pause"); return 0; }
|
process2.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> #include <Windows.h> int main() {
WaitNamedPipe(L"\\\\.\\pipe\\test", NMPWAIT_USE_DEFAULT_WAIT );
HANDLE hpipe = CreateFile(L"\\\\.\\pipe\\test", GENERIC_READ | GENERIC_WRITE, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
char WriteBuffer[] = "Hello World!"; DWORD dwRetSize = 0; WriteFile(hpipe, WriteBuffer, strlen(WriteBuffer) + 1, &dwRetSize, NULL);
char szBuffer[0x100] = { 0 }; DWORD dwSize = 0;
ReadFile(hpipe, szBuffer, 0x100, &dwSize, NULL); printf("%s\r\n", szBuffer);
system("pause"); return 0;
}
|
最后运行结果如下:
三,剪切板
进程通信
的第一种方式:剪切板
剪切板(Clipboard)是操作系统提供的一种临时存储区域,用于在不同应用程序之间传输数据。剪切板可以保存文本、图像、文件、格式化数据等信息,允许用户通过复制、剪切和粘贴操作在不同程序或不同进程之间交换数据。
剪切板的工作原理:
- 复制(Copy):用户选择要复制的数据,操作系统将其保存到剪切板。
- 剪切(Cut):用户选择要剪切的数据,操作系统将数据从源位置移至剪切板,并标记源位置为空(如删除)。
- 粘贴(Paste):用户从剪切板粘贴数据到目标位置,操作系统将数据从剪切板传输到目标应用程序或文件。
常见的剪切板数据类型:
- 文本数据:普通文本(如字符串)或富文本(带格式的文本,如加粗、斜体)。
- 图像数据:位图、图形图像(如JPEG、PNG)。
- 文件数据:文件或文件路径。
- 自定义数据格式:特定应用程序定义的数据格式。
使用剪切板进行进程间通信:
通过剪切板,两个进程(应用程序)可以共享数据。在一个进程中将数据放入剪切板,另一个进程可以读取这些数据。
示例:
process1.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <iostream> #include <Windows.h>
int main() { const char * pStr = "this is a string"; if (OpenClipboard(NULL)) { char * szBuffer = NULL; EmptyClipboard();
HGLOBAL hClip = GlobalAlloc(GHND,strlen(pStr)+1); szBuffer = (char*)GlobalLock(hClip);
strcpy_s(szBuffer, strlen(pStr) + 1, pStr); GlobalUnlock(hClip); SetClipboardData(CF_TEXT, hClip); CloseClipboard();
} system("pause"); return 0; }
|
process2.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <Windows.h> int main() {
if (OpenClipboard(NULL)) { if (IsClipboardFormatAvailable(CF_TEXT)) { HGLOBAL hClip = GetClipboardData(CF_TEXT); char * szBuffer = NULL; szBuffer = (char*)GlobalLock(hClip); GlobalUnlock(hClip); printf("%s\r\n",szBuffer);
} CloseClipboard();
}
system("pause"); return 0;
}
|
我们可以直接运行这段代码,可以看到,剪切板多了如下内容:
我们再写一段代码,来读取剪切板:
process2.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream> #include <Windows.h> int main() {
if (OpenClipboard(NULL)) { if (IsClipboardFormatAvailable(CF_TEXT)) { HGLOBAL hClip = GetClipboardData(CF_TEXT); char * szBuffer = NULL; szBuffer = (char*)GlobalLock(hClip); GlobalUnlock(hClip); printf("%s\r\n",szBuffer);
} else { printf("格式不正确\r\n"); } CloseClipboard();
} system("pause"); return 0;
}
|
我们可以看到其内容如下:
四,WM_COPYDATA
进程通信
的第二种方式:利用WM_COPYDATA
WM_COPYDATA
是 Windows 消息机制中的一种特殊消息,用于进程间通信。它通常用于一个进程将数据发送到另一个进程。与其他消息不同,WM_COPYDATA
允许一个进程通过一个结构体传递数据,而无需通过文件或剪贴板等中介。
基本概念:
WM_COPYDATA
消息的发送者和接收者之间通过一个 COPYDATASTRUCT
结构体进行数据交换。这个结构体包含了数据的指针和数据的大小等信息。
结构体 COPYDATASTRUCT
:
1 2 3 4 5 6
| typedef struct tagCOPYDATASTRUCT { ULONG_PTR dwData; DWORD cbData; PVOID lpData; } COPYDATASTRUCT;
|
发送 WM_COPYDATA
消息:
WM_COPYDATA
消息通过 SendMessage
或 PostMessage
发送。发送者通过该消息将数据传递给接收者。
示例:
首先我们创建两个基于对话框
的新项目:如下:
创建完毕以后点击资源视图
,再点击Dialog
,如下:
他就会弹出一个可视化对话框
,然后双击确定按钮:
然后我写入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| void CwmcopydataDlg::OnBnClickedOk() {
HWND hRecv = ::FindWindow(NULL, _T("123")); CString strDataToSend = _T("HELLO COPY DATA"); if (hRecv != NULL) { COPYDATASTRUCT cpd; cpd.cbData = strDataToSend.GetLength() * sizeof(TCHAR); cpd.dwData = 0; cpd.lpData = (PVOID)strDataToSend.GetBuffer(0); ::SendMessage(hRecv, WM_COPYDATA, (WPARAM)AfxGetApp()->m_pMainWnd, (LPARAM)&cpd); } CDialogEx::OnOK(); }
|
继续打开另外一个项目,然后右键,点击类向导
:
然后将其Caption
设置为123
,如下:
点击消息
,然后输入WM_COPYDATA
:如下
写入如下代码:
1 2 3 4 5 6 7 8 9 10 11
| BOOL Cwmcopydata2Dlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) { LPCTSTR szText = (LPCTSTR)pCopyDataStruct->lpData; DWORD dwLen = pCopyDataStruct->cbData; TCHAR szRecvText[1024] = { 0 }; memcpy(szRecvText, szText, dwLen); MessageBox(szRecvText);
return CDialogEx::OnCopyData(pWnd, pCopyDataStruct); }
|
先运行后者,再运行前者,然后点击前者的确定按钮我们会发现,其弹窗在后者窗口上,成功实现了进程通信。
如下:
左侧是发送端,右侧是接受端,我们可以看到点击发送端确定按钮,就成功发送过去了。
说明:
- 发送端使用
SendMessage
将数据通过 WM_COPYDATA
消息发送给接收端。
- 接收端的窗口过程会处理
WM_COPYDATA
消息,提取数据并可以进行后续处理。
优点:
WM_COPYDATA
是一种相对简单且高效的进程间通信方式,特别适用于跨进程的数据交换。
- 与管道、套接字等方法相比,它更适合小规模的数据传递。
注意:
- 发送的数据需要遵循约定的格式,以便接收端能够正确解码。
- 数据大小和类型需要通过
COPYDATASTRUCT
明确指定。
五,文件映射
进程通信
的第三种方式:文件映射
文件映射(File Mapping)是Windows操作系统提供的一种机制,允许将磁盘上的文件映射到进程的虚拟内存空间,使得文件内容可以像访问内存一样被进程操作。文件映射的一个重要优势是,它能够在多个进程之间共享文件内容,从而实现进程间通信(IPC),并且可以避免频繁的磁盘I/O操作,提高性能。
如下示例图:
我只要修改了这个虚拟内存,就相当于修改了物理页,也就相当于修改了文件内容。
文件映射的工作原理:
通过文件映射,操作系统将磁盘上的文件内容映射到内存中,进程可以直接访问这些映射的内存区域,就像访问普通的内存一样。文件内容并不会立即加载到内存,而是根据访问需要按需加载。这种机制可以减少内存的消耗,尤其是在处理大文件时。
文件映射的关键概念:
- 文件映射对象(File Mapping Object): 文件映射对象是一个在内存中的结构,它表示了文件映射的映射区域。通过文件映射对象,操作系统能够管理文件的内存映射。
- 内存视图(Memory View): 内存视图是应用程序映射到其地址空间的文件的一部分。多个进程可以共享同一个文件映射对象,但每个进程都有自己的内存视图。
文件映射的主要步骤:
- 创建或打开文件映射对象:首先,使用
CreateFileMapping
函数创建一个文件映射对象,或者打开一个已存在的文件映射对象。
- 映射文件到进程的内存:通过
MapViewOfFile
函数将文件的某一部分映射到进程的虚拟地址空间。
- 操作映射的内存:映射到内存中的文件内容可以直接通过指针进行读写。
- 解除映射并关闭文件映射:操作完毕后,通过
UnmapViewOfFile
解除映射,并通过 CloseHandle
关闭文件映射对象。
常用API:
CreateFileMapping:创建或打开一个文件映射对象。
1 2 3 4 5 6 7 8
| HANDLE CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName );
|
MapViewOfFile:将文件映射到进程的内存空间。
1 2 3 4 5 6 7
| LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap );
|
UnmapViewOfFile:解除文件映射。
1
| BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
|
CloseHandle:关闭文件映射对象。
1
| BOOL CloseHandle(HANDLE hObject);
|
示例:
share_process_file.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <iostream> #include <Windows.h>
int main() {
HANDLE hFile = CreateFile(L"1.test", GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE hMapFile = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0x1000, L"test");
LPVOID lpBuffer = MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,0x1000);
*(DWORD*)lpBuffer = 0x12345678;
getchar(); UnmapViewOfFile(lpBuffer); CloseHandle(hMapFile); CloseHandle(hFile);
system("pause"); return 0; }
|
我们直接运行这个程序,发现其成功生成了这个文件:
我们利用二进制编辑器打开看看内容:如下
我们可以看到其内容,如下:
和我们自己写的对应上了。
然后我们可以再写一个程序直接调用这段共有内存:如下
share.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <Windows.h>
int main() { HANDLE hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"test"); LPVOID lpBuffer = MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0x1000);
*(DWORD*)lpBuffer = 0xfffffff1; printf("%d\r\n", *(DWORD*)lpBuffer);
getchar(); UnmapViewOfFile(lpBuffer); CloseHandle(hFileMap); return 0; }
|
最后文件内容变成了如下这样:
成功操作,重写了这段内容。