WIN32 常见IPC方式

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WIN32 常见IPC方式相关的知识,希望对你有一定的参考价值。

前言

以前学了点LinuxandroidIPC后来发现在学Win32平台下的IPC感觉轻松很多,包括什么内核态 用户态的概念。

以下为博主相关android的IPC文章:

相关连接

https://fanmingyi.blog.csdn.net/article/details/115594653

在这里插入图片描述
理解binder一些概念和win32是一样,比如句柄的概念,内存拷贝等

一些简单的前置知识

  1. 进程隔离:
    两个进程地址空间是相互独立,比如A进程内的一个地址:0x00001. 和B进程内的一个地址:0x00001. 两个虽然数值上相同但是他们指向不同物理空间。
    我们来看个小实验加深理解:

在这里插入图片描述

此时我们修改进程1相同内存数值地址的内存值,如果他们指向同一块内存区域那么第二个进程也想对应会变化。

在这里插入图片描述
对于虚拟内存是程序基础知识哦,上面的实验请关闭随机基质.

这里有一篇不错的虚拟内存文章,linux也是类比
Win32之内存管理之虚拟内存跟物理内存

  1. 内核态
    所有API到最后都会陷入操作系统内核完成实际工作,但陷入内核时我们称为内核态。(linux 调用sys_call进入内核具体可以参考网上文献)

  2. 用户态
    简单来说就开发者写的代码在运行的时候状态,成为用户态. 比如你现在在运行println("hello")且没进入内核状态。

内核态的内存是所有进程间共享的,而用户态内存是进程独享的,并且彼此不能读写。如下图:
在这里插入图片描述

上面的图我不知道能否解释清楚,两个进程内存相交区域就是内核空间.所以IPC方式往往都是通过内核空间来来交换数据,但往往会引起多次内存拷贝问题。

WM_COPYDATA方式

这个有点类似Linux下的信号方式,但是比信号可以携带更多的对象信息。

WM_COPYDATA方式流程

  1. 寻找另一个进程的句柄
  2. 给另一个进程句柄发送WM_COPYDATA消息
  3. 内核拷贝WM_COPYDATA消息对应句柄对象到内核内存
  4. 内核最后再将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的文章

我所理解的linux 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来进行匿名内存映射来进行通信。

管道

管道可以在多进程之间构建一个输入或者输出流,在形式上一般右匿名管道命名管道

管道同样需要内存上的拷贝操作,这里就仔细讲解了

匿名管道

没有通过文件作为媒介创建流,一般可以用于父子进程之间的通信。

管道继承msdn文档

我们看下父进程代码:



#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

IPC瓶颈?

在两个 C# 应用程序(32 位和 64 位)之间进行 IPC 的最佳方式是啥

win32是啥?

Win32 进程间通信分配

posix_ipc python 包等效于 Windows?