文章目录
- 前言
- 概述
- 发送消息WM_COPYDATA
- DLL共享段
- 文件映射
- 文件映射步骤
- 相关API讲解
- 文件映射 进程间的通信(有文件版本)
- 文件映射 进程间的通信(匿名版本)
- 管道
- 相关API讲解
- 父子之间的匿名进程通信
- GetStdHandle
- STARTUPINFO指定句柄
- 测试案例
- 模拟CMD
- 总结
前言
- 各位师傅大家好,我是qmx_07,今天给大家讲解进程间通信的相关知识
概述
- 进程间通信(Inter-Process Communication,IPC)是指不同进程之间进行数据交换、信息共享、同步互斥等操作的机制
发送消息WM_COPYDATA
- 介绍:WM_COPYDATA是一个Windows消息,用于在进程之间传递数据,使用结构体COPYDATASTRUCT
typedef struct tagCOPYDATASTRUCT {ULONG_PTR dwData;//自定义数据,可以用于标识传递的数据类型或其他用途DWORD cbData;//数据大小PVOID lpData;//传递的数据
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
- 通过SendMessage发送消息
案例:创建两个项目工程,发送方 和 接收方
发送方:
void CSendDlg::OnBnClickedSend()
{//获取编辑框内容CString strText;GetDlgItemText(IDC_EDIT, strText);//获取窗口句柄HWND hWnd = ::FindWindow(NULL, "recvcopy");//编辑要发送的数据COPYDATASTRUCT cds;cds.dwData = 0x12345678;cds.cbData = strText.GetLength();cds.lpData = strText.GetBuffer();//发送数据::SendMessage(hWnd, WM_COPYDATA, (WPARAM)GetSafeHwnd(),(LPARAM)&cds);// TODO: 在此添加控件通知处理程序代码
}
接收方:
- 通过类向导,创建WM_COPYDATA消息
BOOL CreceiveDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{// TODO: 在此添加消息处理程序代码和/或调用默认值CString strFmt;strFmt.Format("%08X %s",pCopyDataStruct->dwData,pCopyDataStruct->lpData);AfxMessageBox(strFmt);return CDialogEx::OnCopyData(pWnd, pCopyDataStruct);
}
- 接收数据,弹窗输出
画面演示:
使用WM_COPYDATA消息进行进程间通信,我修改A进程的发送数据,B进程接收到数据会受影响吗?
如果A进程通过WM_COPYDATA消息向B进程发送数据,那么B进程接收到的数据不会受到A进程后续的修改影响。因为WM_COPYDATA消息是将A进程的数据拷贝一份并发送给B进程的,之后B进程操作的是自己的一份拷贝,和A进程的数据无关。所以,A进程修改数据不会影响B进程接收到的数据
其中,发送进程通过发送WM_COPYDATA消息向接收进程发送数据。在接收进程接收到数据后,发生了两次内存拷贝,第一次是将数据拷贝至共享内存中,第二次是从共享内存中读取数据。最后,接收进程向发送进程发送响应消息。WM_COPYDATA可以携带少量数据;效率比较低 ,是因为WM_COPYDATA先将数据拷贝到高2G系统内存中,再从高2G内存中拷贝到目标进程中,发生两次拷贝
缺点:
- 数据的传输是单向的。
- 效率低:从A进程拷贝到B进程 数据拷贝了 2 次。先将数据拷贝到高 2 G内存中,然后再从 高 2 G拷贝到目标进程中(发生两次拷贝,所以效率比较低)
- 必须是带有窗口之间才能通信(且必须有标题);
- 消息大小受限:WM_COPYDATA 消息传递的数据大小受到系统限制,默认为 4MB。如果需要传递更大的数据,就需要将数据分成多个块来传递
原理:通过系统的高2G内存来达到传输,因为高 2 G的内存是所有引用共享的。
适用场景:数据小,发送频繁等不建议使用,大小不限制
DLL共享段
- 介绍:当一个进程加载一个 DLL 时,该进程会将该 DLL 的实例映射到该进程的虚拟地址空间中,虽然每个进程都有自己的虚拟地址空间,但是它们共享同一个物理内存,这样就可以实现共享代码和数据的目的
- 好处:节省了内存空间,因为多个进程可以共享同一份物理内存,这可以减少系统资源的消耗
- 定义共享段,并定义导出变量,注意导出需要初始化,未初始化不给实际内存
//为共享数据开辟空间
#pragma data_seg("cr41")
_declspec(dllexport) int g_nVal = 0;
#pragma data_seg()
- 链接选项将此共享段声明为可共享
//为开辟的空间 设置共享
#pragma comment(linker,"/SECTION:cr41,RWS")
- 将共享段导出,设置可读、可写、可执行权限
使用方:
#pragma comment(lib,"Dll.lib")
_declspec(dllimport) extern int g_nVal;
void CUseDllDlg::OnBnClickedShow()
{SetDlgItemInt(EDT_SHOW, g_nVal);// TODO: 在此添加控件通知处理程序代码
}void CUseDllDlg::OnBnClickedWrite()
{g_nVal = GetDlgItemInt(EDT_WRITE);// TODO: 在此添加控件通知处理程序代码
}
- 注意:如果全局变量不初始化的话,进程间不共享
- 已经初始化为0的全局变量通常被编译器放在数据段(.data段)中,因此它们在DLL加载到内存时已经被初始化为0。因为它们已经被初始化,所以它们的值可以被不同的进程共享,而且不同进程中的值都是相同的
- 未初始化的全局变量通常被编译器放在BSS段(.bss段)中,这些变量在程序加载时会被清零。因为它们在程序加载时才被初始化,所以它们的值在不同的进程中是不同的,不能被不同进程共享
文件映射
- 介绍:文件映射是一种进程间通信的机制,可以通过将一个文件映射到多个进程的虚拟地址空间来实现数据的共享。在文件映射中,可以分为有文件和无文件的区别
- 有文件:指将一个实际的文件映射到多个进程的虚拟地址空间中,在这种情况下,文件映射的数据是持久的,即文件的内容在进程结束后仍然存在
- 无文件:指在进程间通信中使用文件映射的机制,但并不依赖于实际的文件,进程可以通过创建一个匿名的文件映射对象,将其映射到多个进程的虚拟地址空间中。在这种情况下,映射的数据源并不是一个实际的文件,而是系统内存中的一块共享内存区域,无文件的文件映射的数据是临时的,即在进程结束后会被释放
- 适用场景:无文件的文件映射通常用于临时共享数据、进程间通信等场景,不需要将数据持久保存在文件中
文件映射步骤
- 打开文件:首先,需要打开需要操作的文件,可以使用标准的文件操作函数,如CreateFile等来打开文件。
- 创建文件映射对象:使用CreateFileMapping函数创建一个文件映射对象,该函数会返回一个句柄,该句柄可以用于后续的文件映射操作。
- 映射文件到内存:使用MapViewOfFile函数将文件映射到内存中,该函数也会返回一个指针,该指针指向文件在内存中的起始位置。
- 执行读写操作:通过操作内存中的数据来进行读写操作,内存中的数据会自动同步到文件中。
- 取消文件映射:使用UnmapViewOfFile函数取消文件映射,释放内存空间。
- 关闭文件句柄:使用CloseHandle函数关闭文件句柄和文件映射对象句柄。
相关API讲解
映射文件对象:
HANDLE CreateFileMappingW([in] HANDLE hFile,//文件句柄[in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,//安全描述符,填写NULL 为默认安全[in] DWORD flProtect,//文件映射对象的页保护[in] DWORD dwMaximumSizeHigh,//高地址位[in] DWORD dwMaximumSizeLow,//低地址位[in, optional] LPCWSTR lpName//文件映射对象的名称,为NULL创建匿名的映射对象
);
页保护对象值:
将文件对象映射到内存:
LPVOID MapViewOfFile([in] HANDLE hFileMappingObject,//文件映射对象的句柄[in] DWORD dwDesiredAccess,//文件映射对象的访问类型,比如读、写、执行[in] DWORD dwFileOffsetHigh,//高地址位[in] DWORD dwFileOffsetLow,//低地址位[in] SIZE_T dwNumberOfBytesToMap//要映射的文件字节数
);
文件映射 进程间的通信(有文件版本)
A方:
#include <iostream>
#include <Windows.h>using namespace::std;int main()
{//打开文件HANDLE hFile = CreateFile(R"(G:\c_study\Dll\Debug\Dll.dll)",GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);//判断文件是否正常打开if (hFile == INVALID_HANDLE_VALUE){cout << "打开文件失败!" << endl;return 0;}//映射对象HANDLE hFileMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0, 0,//操作整个文件"cr41map"//映射对象名称);//判断映射对象是否成功if (hFileMap == NULL){cout << "文件映射对象失败" << endl;CloseHandle(hFile);return 0;}//映射LPVOID pView = MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS,0, 0,0x1000);//映射是否成功if (pView == NULL){cout << "映射失败" << endl;}//清理资源UnmapViewOfFile(pView);CloseHandle(hFileMap);CloseHandle(hFile);
}
B方:
#include <iostream>
#include <Windows.h>using namespace::std;int main()
{//映射对象HANDLE hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,"cr41map");//判断映射对象是否成功if (hFileMap == NULL){cout << "文件映射对象失败!" << endl;return 0;}//映射LPVOID pView = MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS,0, 0,0x1000);//映射是否成功if (pView == NULL){cout << "映射失败" << endl;}//清理资源UnmapViewOfFile(pView);CloseHandle(hFileMap);
}
演示:
文件映射 进程间的通信(匿名版本)
A方:
#include <iostream>
#include <Windows.h>using namespace::std;int main()
{//映射对象HANDLE hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0, 0x1000,"cr41map");//判断映射对象是否成功if (hFileMap == NULL){cout << "文件映射对象失败" << endl;return 0;}//映射LPVOID pView = MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS,0, 0,0x1000);//映射是否成功if (pView == NULL){cout << "映射失败" << endl;}//清理资源UnmapViewOfFile(pView);CloseHandle(hFileMap);
}
B方:
#include <iostream>
#include <Windows.h>using namespace::std;int main()
{//映射对象HANDLE hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,"cr41map");//判断映射对象是否成功if (hFileMap == NULL){cout << "文件映射对象失败!" << endl;return 0;}//映射LPVOID pView = MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS,0, 0,0x1000);//映射是否成功if (pView == NULL){cout << "映射失败" << endl;}//清理资源UnmapViewOfFile(pView);CloseHandle(hFileMap);}
演示:
管道
- 概念:可跨进程的队列,实现进程之间的数据传输
- 种类:命名管道,匿名管道
命名管道:是一种有名字的管道,它可以被多个进程同时使用,命名管道适用于本地进程之间的通信和远程进程之间的通信
匿名管道:命名管道适用于本地进程之间的通信和远程进程之间的通信,主要用于父子进程之间的数据传输。(单向传输)
- 父进程读取的管道以及子进程读取的管道,相应的子进程也可以对父进程读取的管道进行传输数据
相关API讲解
创建匿名管道:
BOOL CreatePipe([out] PHANDLE hReadPipe,//管道读取句柄[out] PHANDLE hWritePipe,//管道写入句柄[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,//安全属性,NULL为默认的安全属性[in] DWORD nSize//填写0,为默认的缓冲区长度
);
查看管道是否有数据可读:
BOOL PeekNamedPipe([in] HANDLE hNamedPipe,//管道句柄[out, optional] LPVOID lpBuffer,//缓冲区指针,用于读取数据[in] DWORD nBufferSize,//缓冲区大小[out, optional] LPDWORD lpBytesRead,//用于存储已读取的缓冲区大小[out, optional] LPDWORD lpTotalBytesAvail,//存储可用的字节数大小[out, optional] LPDWORD lpBytesLeftThisMessage//用于存储当前消息中剩余的字节数。该参数只对消息式管道有效,对字节流式管道没有意义
);
- 若要从管道读取,请在调用 ReadFile 函数时使用管道的读取句柄
- 若要写入管道,请在调用 WriteFile 函数时使用管道的写入句柄
- 如果管道缓冲区已满,并且有更多的字节需要写入,则 WriteFile 不会返回,直到另一个进程从管道中读取内容,从而提供更多的缓冲区空间。 管道服务器在调用 CreatePipe时指定管道的缓冲区大小。
父子之间的匿名进程通信
步骤:
- 获取标准输入输出的句柄
- 继承句柄
- 创建管道的时候;
- 创建子窗口的时候;
- 创建进程时需要STARTUPINFO指定句柄
GetStdHandle
**介绍:**获取标准输入、标准输出或标准错误设备的句柄
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle
);
STARTUPINFO指定句柄
- 通过设置STARTUPINFO的输入输出,来获取句柄
测试案例
- 父进程:
oid CParentDlg::OnBnClickedCreate()
{SECURITY_ATTRIBUTES sa = { };sa.nLength = sizeof(sa);sa.bInheritHandle = TRUE;BOOL bRet = CreatePipe(&m_hChildRead, &m_hParentWrite, &sa, 0);if (!bRet){AfxMessageBox("创建管道失败!");}bRet = CreatePipe(&m_hParentRead, &m_ChildWrite, &sa, 0);if (!bRet){AfxMessageBox("创建管道失败!");}STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = m_hChildRead;//子进程读数据的句柄si.hStdOutput = m_ChildWrite;//子进程写数据的句柄ZeroMemory(&pi, sizeof(pi));if (!CreateProcess(NULL,R"(G:\c_study\test_pipe\Chlid\Debug\Chlid.exe)",&sa,NULL,TRUE,0,NULL,NULL,&si,&pi)){AfxMessageBox("CreateProcess Failed");}CloseHandle(pi.hProcess);CloseHandle(pi.hThread);// TODO: 在此添加控件通知处理程序代码
}
- 创建模块:设置安全属性,创建两条管道,创建child进程 允许继承
oid CParentDlg::OnBnClickedWrite()
{// TODO: 在此添加控件通知处理程序代码CString strBuf;GetDlgItemText(EDT_WRITE, strBuf);DWORD dwBytesWrited = 0;BOOL bRet = WriteFile(m_hParentWrite,strBuf.GetBuffer(),strBuf.GetLength() + 1,&dwBytesWrited,NULL);if (!bRet){AfxMessageBox("写入失败!");}}
- 写入模块: 获取EDT内容,通过writeFile写入管道
void CParentDlg::OnBnClickedRead()
{DWORD dwAvailNum = 0;//管道剩余大小BOOL bRet = PeekNamedPipe(m_hParentRead, NULL, 0, NULL, &dwAvailNum, NULL);if (!bRet){return;}if (dwAvailNum > 0){CString strBuf;DWORD dwBytesReaded = 0;BOOL bRet = ReadFile(m_hParentRead,strBuf.GetBufferSetLength(dwAvailNum),dwAvailNum,&dwBytesReaded,NULL);strBuf.ReleaseBuffer(dwBytesReaded);if (!bRet){AfxMessageBox("读取失败!");}else{SetDlgItemText(EDT_READ, strBuf);}}else{AfxMessageBox("没有数据可读!");}// TODO: 在此添加控件通知处理程序代码
}
- 读取模块: 判断 管道内是否有数据,通过读取管道 写入到 文本框中
子进程:
void CChlidDlg::OnBnClickedRead()
{HANDLE hChildRead = GetStdHandle(STD_INPUT_HANDLE);DWORD dwAvailNum = 0;//管道剩余大小BOOL bRet = PeekNamedPipe(hChildRead, NULL, 0, NULL, &dwAvailNum, NULL);if (!bRet){return;}if (dwAvailNum > 0){CString strBuf;DWORD dwBytesReaded = 0;BOOL bRet = ReadFile(hChildRead,strBuf.GetBufferSetLength(dwAvailNum),dwAvailNum,&dwBytesReaded,NULL);strBuf.ReleaseBuffer(dwBytesReaded);if (!bRet){AfxMessageBox("读取失败!");}else{SetDlgItemText(EDT_READ, strBuf);}}else{AfxMessageBox("没有数据可读!");}// TODO: 在此添加控件通知处理程序代码
}
void CChlidDlg::OnBnClickedWrite()
{HANDLE m_ChildWrite = GetStdHandle(STD_OUTPUT_HANDLE);CString strBuf;GetDlgItemText(EDT_WRITE, strBuf);DWORD dwBytesWrited = 0;BOOL bRet = WriteFile(m_ChildWrite,strBuf.GetBuffer(),strBuf.GetLength() + 1,&dwBytesWrited,NULL);if (!bRet){AfxMessageBox("写入失败!");}// TODO: 在此添加控件通知处理程序代码
}
- 与父进程的读取管道,写入管道 基本一致,需要改改句柄
画面演示:
模拟CMD
- 设置多行文本框、设置水平和纵向下拉框
void CParentDlg::OnBnClickedCreate()
{SECURITY_ATTRIBUTES sa = { };sa.nLength = sizeof(sa);sa.bInheritHandle = TRUE;BOOL bRet = CreatePipe(&m_hChildRead, &m_hParentWrite, &sa, 0);if (!bRet){AfxMessageBox("创建管道失败!");}bRet = CreatePipe(&m_hParentRead, &m_ChildWrite, &sa, 0);if (!bRet){AfxMessageBox("创建管道失败!");}STARTUPINFO si;PROCESS_INFORMATION pi;ZeroMemory(&si, sizeof(si));si.cb = sizeof(si);si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = m_hChildRead;//子进程读数据的句柄si.hStdOutput = m_ChildWrite;//子进程写数据的句柄ZeroMemory(&pi, sizeof(pi));if (!CreateProcess(NULL,"cmd.exe",&sa,NULL,TRUE,CREATE_NO_WINDOW,//无窗口打开NULL,NULL,&si,&pi)){AfxMessageBox("CreateProcess Failed");}CloseHandle(pi.hProcess);CloseHandle(pi.hThread);// TODO: 在此添加控件通知处理程序代码
}
- 设置打开cmd程序,设置 无窗口打开程序
void CParentDlg::OnBnClickedWrite()
{// TODO: 在此添加控件通知处理程序代码CString strBuf;GetDlgItemText(EDT_WRITE, strBuf);strBuf += "\r\n";//换行DWORD dwBytesWrited = 0;BOOL bRet = WriteFile(m_hParentWrite,strBuf.GetBuffer(),strBuf.GetLength(),//将+1取消,不要读取\0&dwBytesWrited,NULL);if (!bRet){AfxMessageBox("写入失败!");}}
- 写入文本换行,不要读取\0
void CParentDlg::OnBnClickedRead()
{DWORD dwAvailNum = 0;//管道剩余大小BOOL bRet = PeekNamedPipe(m_hParentRead, NULL, 0, NULL, &dwAvailNum, NULL);if (!bRet){return;}if (dwAvailNum > 0){CString strBuf;DWORD dwBytesReaded = 0;BOOL bRet = ReadFile(m_hParentRead,strBuf.GetBufferSetLength(dwAvailNum),dwAvailNum,&dwBytesReaded,NULL);strBuf.ReleaseBuffer(dwBytesReaded);if (!bRet){AfxMessageBox("读取失败!");}else{SetDlgItemText(EDT_READ, strBuf);}}else{AfxMessageBox("没有数据可读!");}// TODO: 在此添加控件通知处理程序代码
}
- 和之前的代码逻辑一致
画面演示:
总结
- 介绍了常用的进程通信方式:WM_COPYDATA、DLL共享段、文件映射、管道的相关知识