WIN32 常见IPC方式
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WIN32 常见IPC方式相关的知识,希望对你有一定的参考价值。
前言
以前学了点Linux
和android
的IPC
后来发现在学Win32
平台下的IPC感觉轻松很多,包括什么内核态
用户态
的概念。
以下为博主相关android的IPC文章:
相关连接
https://fanmingyi.blog.csdn.net/article/details/115594653
理解binder一些概念和win32是一样,比如句柄的概念,内存拷贝等
一些简单的前置知识
进程隔离
:
两个进程地址空间是相互独立,比如A进程内的一个地址:0x00001
. 和B进程内的一个地址:0x00001
. 两个虽然数值上相同但是他们指向不同物理空间。
我们来看个小实验加深理解:
此时我们修改进程1
相同内存数值地址的内存值,如果他们指向同一块内存区域那么第二个进程也想对应会变化。
对于虚拟内存是程序基础知识哦,上面的实验请关闭随机基质
.
这里有一篇不错的虚拟内存文章,linux也是类比
Win32之内存管理之虚拟内存跟物理内存
-
内核态
所有API到最后都会陷入操作系统内核
完成实际工作,但陷入内核时我们称为内核态。(linux 调用sys_call进入内核具体可以参考网上文献) -
用户态
简单来说就开发者写的代码在运行的时候状态,成为用户态
. 比如你现在在运行println("hello")
且没进入内核状态。
内核态的内存是所有进程间共享的,而用户态内存是进程独享的,并且彼此不能读写。如下图:
上面的图我不知道能否解释清楚,两个进程内存相交区域就是内核空间.所以IPC
方式往往都是通过内核空间来来交换数据,但往往会引起多次内存拷贝问题。
WM_COPYDATA方式
这个有点类似Linux下的信号方式,但是比信号可以携带更多的对象信息。
WM_COPYDATA
方式流程
- 寻找另一个进程的句柄
- 给另一个进程句柄发送
WM_COPYDATA
消息 - 内核拷贝
WM_COPYDATA
消息对应句柄对象到内核内存
- 内核最后再将
WM_COPYDATA
所在内核内存拷贝到另一个进程用户空间。
//发送进程
void CMFCOneProcedureDlg::OnBnClickedButton1()
{
//寻找另一个进程句柄 这里是寻找一个叫"MyTwo"标题句柄
HWND hwnd=::FindWindow(NULL, _TEXT("MyTwo"));
//构造对象,这个对象内存会被拷贝到内核内存
CString str(_TEXT("你好我是China"));
//WM_COPYDATA消息结构体
COPYDATASTRUCT cdt;
//这个结构体会被完整拷贝
cdt.dwData = 0x234;
cdt.cbData = str.GetLength()*sizeof(wchar_t)+1;
cdt.lpData = str.GetBuffer();
AfxMessageBox(_T("发送成功"+ str));
//发送到指定进程。此处会陷入内核
::SendMessage(hwnd,WM_COPYDATA, (WPARAM)GetSafeHwnd(),(LPARAM)&cdt);
}
//接收信息的进程
//MFC中默认接收WM_COPYDATA的函数
BOOL CMFCTwoProcedureDlgDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct)
{
//std::string str;
CString str;
//COPYDATASTRUCT这个结构体会被内核拷贝出来
str.Format(_TEXT("%08x data %s"), pCopyDataStruct->dwData, pCopyDataStruct->lpData);
AfxMessageBox(str);
return CDialogEx::OnCopyData(pWnd, pCopyDataStruct);
}
内核拷贝方式,需要A进程拷入内核,然后再从内核考入B进程。效率极低
dll
共享段
我们常规的程序会共享dll
内存,但是写入时会拷贝dll
内存信息到程序用户空间(写时拷贝)。但是我们可以修改连接器的属性从而改变写时拷贝的现象,从而实现内存共享。
我们看下常规情况下动态库引入会对内存影响(linux so
是一样的效果,都是引用,且写时拷贝)
- dll写时拷贝机制:
假设我们dll库中有一个变量如下:
int dllInt=23;
我们的A进程修改dllInt
,请问是否会对另一个加载dll的进程有影响?答案是后续进程和其他进程都只会读取到int dllInt=23;
//A进程修改dll声明的变量
//eport是c++语法的中前置声明关键字,说这个变量会在连接的时候查找到
export int dllInt;
dllInt=111;
A进程由于修改dllInt
数值所以进行拷贝dllInt这块内存在自己用户空间
中,下次读取的时候直接从自己内存读取而不是引用到dll所在内存。
所以我们想利用Dll进行进程通信,就需要打破写死拷贝的规矩
,其声明语法如下:
首先我们先写一个dll库,并告诉系统我们库中部分内存是共享到程序之间
//定义这个section名字叫MyShareSegction
#pragma data_seg("MyShareSegction")
__declspec(dllexport) DWORD myShareDword=0;
#pragma data_seg()
//告诉连接器这个section内存区域具有可读可写可共享,所以不会触发写时拷贝
//https://docs.microsoft.com/en-us/cpp/build/reference/section-specify-section-attributes?view=msvc-160
#pragma comment(linker,"/SECTION:MyShareSegction,RWS")
我们设计一个程序,从编辑框读取一个int数值写入到上面的dll的myShareDword
中,然后再开一个程序读取。
//导入动态库
#pragma comment(lib,"ShareDll.lib")
//声明变量
extern __declspec(dllimport) DWORD myShareDword;
//写入共享段
void CMFCTwoProcedureDlgDlg::OnBnClickedButton1()
{
CString msg;
GetDlgItemText(IDC_EDIT1, msg);
//mfc字符串转化到int
int x = atoi(msg);
//写入共享变量中
myShareDword = x;
}
//点击读取共享按钮弹出信息
void CMFCTwoProcedureDlgDlg::OnBnClickedButton2()
{
//利用mfc字符串读取共享dll变量
CString msg;
msg.Format("%ld", myShareDword);
//弹出
MessageBox(msg);
}
此时我们打开两个进程,一个写入2323
,然后另一个进程点击读取共享
按钮
文件映射
将一个文件内容映射到用户进程的内存空间中,而从实现零拷贝和进程共享这个和linux
下的mmap
是一样的.修改内存会同步到文件上,同样修改文件也会反应到用户进程内存
这个内容和linux的mmap差不多仅仅是API不同,所以可以看博主mmap的文章
https://fanmingyi.blog.csdn.net/article/details/114762889
int main()
{
//我们打开一个文件
HANDLE hFile = ::CreateFile(
//一个文件名称
"D:\\\\安装说明(必看).jpg",
//打开的文件要做什么操作
//比如可读可写
GENERIC_READ | GENERIC_WRITE,
//可用于多进程同时打开文件时的互斥操作,比如传0其他程序不能打开。
//FILE_SHARE_READ后续打开的文件进程只能读
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
//可以指定是否句柄可以被继承,传null不可继承
NULL,
//对于文件不存在的时候相应操作,比如直接覆盖还是直接打开
//CREATE_ALWAYS:不管是否存在都会创建新文件,如果存在旧文件会覆盖,也就是清空旧文件
OPEN_EXISTING,
//设置文件一些特殊属性,比如这个文件应用程序可以读取但是不能写入或者删除,这个属性会被写入元数据中
FILE_ATTRIBUTE_NORMAL,
//扩展属性可以先忽略
NULL
);
//判断文件是否成功打开
if (hFile == INVALID_HANDLE_VALUE)
{
cout << "失败" << endl;
}
//文件映射 https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga
HANDLE hfileMap = CreateFileMapping(
hFile,//可以传入文件句柄或者INVALID_HANDLE_VALUE表示无名映射
NULL,//指定当前返回句柄是否可以被继承。NULL表示不可以被继承
//此参数用于指定被映射的文件 是可读还是可写的等,这个参数需要和文件句柄的访问权限对应
//PAGE_EXECUTE_READ 具有可读可执行权限,写入进行拷贝
PAGE_READWRITE,//
0,//映射多大的内存 的高位 dword
0,//映射多大的内存 的低位 dword 0表示文件的大小
"hellofmy" //文件映射对象的名称。如果已经有进程创建了一个映射那么不在使用自己文件句柄映射,而是使用已经存在别的进程的映射
);
//成功返回不为NULL
if (hfileMap == NULL)
{
::CloseHandle(hFile);
return EXIT_FAILURE;
}
//现在我们有了映射关系配置,但是还需要指定映射文件的那个偏移位置
LPVOID pBuff = ::MapViewOfFile(hfileMap,
FILE_MAP_ALL_ACCESS,//你映射这段内存 主要用途,FILE_MAP_ALL_ACCESS表示映射读写都会反馈到文件上,这个权限必须被hfileMap允许
0,//文件偏移的高位字节
0,//文件偏移的低位字节 注意偏移量必须是windows所定义内存分配粒度的倍数,具体用GetSystemInfo获取
0x10000//你要映射多大的字节 这个数值必须是CreateFileMapping所指定的大小范围内的。0表示整个可以获取的大小
);
if (pBuff == NULL)
{
::CloseHandle(hfileMap);
::CloseHandle(hFile);
return EXIT_FAILURE;
}
UnmapViewOfFile(pBuff);
CloseHandle(hfileMap);
//关闭文件指针
CloseHandle(hFile);
}
我们看下图:
pBuff
是映射的地址,左图是进程内存,右图是winhex打开的文件16进制内容.
我们现在在vs内存窗口修改内存如下图:
vs修改内存后右图的winhenx文件内容也跟着变化.
我们现在来完成一次跨进程的共享Demo
进程1:
//第一个进程
int main()
{
HANDLE hFile = ::CreateFile(
//一个文件名称
"D:\\\\安装说明(必看).jpg",
//打开的文件要做什么操作
//比如可读可写
GENERIC_READ | GENERIC_WRITE,
//可用于多进程同时打开文件时的互斥操作,比如传0其他程序不能打开。
//FILE_SHARE_READ后续打开的文件进程只能读
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
//可以指定是否句柄可以被继承,传null不可继承
NULL,
//对于文件不存在的时候相应操作,比如直接覆盖还是直接打开
//CREATE_ALWAYS:不管是否存在都会创建新文件,如果存在旧文件会覆盖,也就是清空旧文件
OPEN_EXISTING,
//设置文件一些特殊属性,比如这个文件应用程序可以读取但是不能写入或者删除,这个属性会被写入元数据中
FILE_ATTRIBUTE_NORMAL,
//扩展属性可以先忽略
NULL
);
//判断文件是否成功打开
if (hFile == INVALID_HANDLE_VALUE)
{
cout << "失败" << endl;
}
//文件映射 https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga
HANDLE hfileMap = CreateFileMapping(
hFile,//可以传入文件句柄或者INVALID_HANDLE_VALUE表示无名映射
NULL,//指定当前返回句柄是否可以被继承。NULL表示不可以被继承
//此参数用于指定被映射的文件 是可读还是可写的等,这个参数需要和文件句柄的访问权限对应
//PAGE_EXECUTE_READ 具有可读可执行权限,写入进行拷贝
PAGE_READWRITE,//
0,//映射多大的内存 的高位 dword
0,//映射多大的内存 的低位 dword 0表示文件的大小
"hellofmy" //文件映射对象的名称。如果已经有进程创建了一个映射那么不在使用自己文件句柄映射,而是使用已经存在别的进程的映射
);
//成功返回不为NULL
if (hfileMap == NULL)
{
::CloseHandle(hFile);
return EXIT_FAILURE;
}
//现在我们有了映射关系配置,但是还需要指定映射文件的那个偏移位置
LPVOID pBuff = ::MapViewOfFile(hfileMap,
FILE_MAP_ALL_ACCESS,//你映射这段内存 主要用途,FILE_MAP_ALL_ACCESS表示映射读写都会反馈到文件上,这个权限必须被hfileMap允许
0,//文件偏移的高位字节
0,//文件偏移的低位字节 注意偏移量必须是windows所定义内存分配粒度的倍数,具体用GetSystemInfo获取
0x10000//你要映射多大的字节 这个数值必须是CreateFileMapping所指定的大小范围内的。0表示整个可以获取的大小
);
if (pBuff == NULL)
{
::CloseHandle(hfileMap);
::CloseHandle(hFile);
return EXIT_FAILURE;
}
UnmapViewOfFile(pBuff);
CloseHandle(hfileMap);
//关闭文件指针
CloseHandle(hFile);
}
//第二个进程 采用INVALID_HANDLE_VALUE和制定文件映射名称的方式来关联到第一个进程创建的映射
int main()
{
//文件映射 https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createfilemappinga
HANDLE hfileMap = CreateFileMapping(
INVALID_HANDLE_VALUE,//可以传入文件句柄或者INVALID_HANDLE_VALUE表示无名映射,INVALID_HANDLE_VALUE必须指定映射大小
NULL,//指定当前返回句柄是否可以被继承。NULL表示不可以被继承
//此参数用于指定被映射的文件 是可读还是可写的等,这个参数需要和文件句柄的访问权限对应
//PAGE_EXECUTE_READ 具有可读可执行权限,写入进行拷贝
PAGE_READWRITE,//
0,//映射多大的内存 的高位 dword
0x10000,//映射多大的内存 的低位 dword 0表示文件的大小
"hellofmy" //文件映射对象的名称。如果已经有进程创建了一个映射那么不在使用自己文件句柄映射,而是使用已经存在别的进程的映射
);
//现在我们有了映射关系配置,但是还需要指定映射文件的那个偏移位置
LPVOID pBuff = ::MapViewOfFile(hfileMap,
FILE_MAP_ALL_ACCESS,//你映射这段内存 主要用途,FILE_MAP_ALL_ACCESS表示映射读写都会反馈到文件上,这个权限必须被hfileMap允许
0,//文件偏移的高位字节
0,//文件偏移的低位字节 注意偏移量必须是windows所定义内存分配粒度的倍数,具体用GetSystemInfo获取
0x10000//你要映射多大的字节 这个数值必须是CreateFileMapping所指定的大小范围内的。0表示整个可以获取的大小
);
if (pBuff == NULL)
{
::CloseHandle(hfileMap);
return EXIT_FAILURE;
}
UnmapViewOfFile(pBuff);
CloseHandle(hfileMap);
}
两个进程内存图:
修改第一个进程内存:
第二个进程和文件内容跟着变化。
当然你也可以让两个进程同时使用INVALID_HANDLE_VALUE
来进行匿名内存映射来进行通信。
管道
管道可以在多进程之间构建一个输入或者输出流,在形式上一般右匿名管道
和命名管道
。
管道同样需要内存上的拷贝操作,这里就仔细讲解了
匿名管道
没有通过文件作为媒介创建流,一般可以用于父子进程之间的通信。
我们看下父进程代码:
#include <iostream>
#include <Windows.h>
#include<tlhelp32.h>
#include <string>
using namespace std;
int main()
{
//子进程发送给父进程管道流
HANDLE m_hRead;
HANDLE m_hWrite;
//父进程发送子进程的管道流
HANDLE m_child_hRead;
HANDLE m_child_hWrite;
//必须指定当前句柄是可以被继承
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE;
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(sa);
//创建句柄
BOOL result = ::CreatePipe(&m_hRead, &m_hWrite, &sa, 0);
if (!result)
{
return EXIT_FAILURE;
}
//创建句柄
result = CreatePipe(&m_child_hRead, &m_child_hWrite, &sa, 0);
if (!result)
{
return EXIT_FAILURE;
}
//开启子进程代码
STARTUPINFO si;
PROCESS_INFORMATION pi;
//
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
//通过这个参数将管道的两个句柄发送给子进程
//当然这只是其中一种方式
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdOutput = m_hWrite;
si.hStdInput = m_child_hRead;
//子进程名字
char name[] = "ConsoleApplication2.exe";
// Start the child process.
if (!CreateProcess(NULL, // No module name (use command line)
name, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
TRUE, // 指示当前句柄可以继承
CREATE_NEW_CONSOLE, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi) // Pointer to PROCESS_INFORMATION structure
)
{
printf("CreateProcess failed (%d).\\n", GetLastError());
return EXIT_SUCCESS;
}
// Close process and thread handles.
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
//开始父子进程的通信
DWORD dwBytesToRead = 0;
char buf[1024 * 10] = { 0 };
int i = 0;
while (true)
{
//读取子进程发来的信息
::ReadFile(m_hRead, buf, sizeof buf, &dwBytesToRead, NULL);
cout << "父进程:" << buf << endl;
//写入到子进程
string msg = "你好我是父进程,我现在发送这个信息给你";
msg += std::to_string(++i);
msg += "\\r\\n";
const char * helloBuffer = msg.c_str();
::WriteFile(m_child_hWrite, helloBuffer, strlen(helloBuffer) + 1, &dwBytesToRead, NULL);
}
CloseHandle(m_hRead);
CloseHandle(m_hWrite);
CloseHandle(m_child_hRead);
CloseHandle(m_child_hWrite);
return EXIT_SUCCESS;
}
子进程代码
#include <iostream>
#include <Windows.h>
#include<fileapi.h>
using namespace std;
#include <string> // std::string, std::to_string
int main(int argc, char argv[])
{
//读取父进程传入的句柄
//使用read可以读取父进程发来的信息
HANDLE read = GetStdHandle(STD_INPUT_HANDLE);
//使用write可以写入信息到父进程
HANDLE write = GetStdHandle(STD_OUTPUT_HANDLE);
//这里主要是用于恢复标准的输入输出
HANDLE hStdout = CreateFile("CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE hStdin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
SetStdHandle(STD_OUTPUT_HANDLE, hStdout);
SetStdHandle(STD_ERROR_HANDLE, hStdout);
SetStdHandle(STD_INPUT_HANDLE, hStdin);
//父子进程通信
DWORD dwBytesToRead = 0;
char buf[1024 * 10] = {0};
int i = 0;
while (true)
{
string msg = "你好我是子进程,我现在发送这个信息给你";
msg += std::to_string(++i);
const char * helloBuffer = msg.c_str();
::WriteFile(write, helloBuffer, strlen(helloBuffer) + 1, &dwBytesToRead, NULL);
::ReadFile(read, buf, sizeof(buf), &dwBytesToRead, NULL);
//输出到屏幕
::WriteFile(hStdout, buf, strlen(buf) + 1, &dwBytesToRead, NULL);
Sleep(1000);
}
CloseHandle(read);
CloseHandle(write);
CloseHandle(hStdout);
CloseHandle(hStdin);
return 0;
}
两路 IPC 的 RCF