0xAA55 发表于 2017-11-19 04:45:57

【翻译】MSDN资料:进程间通讯方法

MSDN原文网址:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365574(v=vs.85).aspx

打下划线的地方是我自己说的话,而其他的则基本基于谷歌翻译。

这个Windows操作系统支持了很多机制用于进程间数据共享。我们把这种行为称作进程间通讯(IPC)。有些进程间通讯支持一台电脑里特定进程之间的进程通讯,而还有的进程间通讯则支持跨电脑、跨网络的进程通讯。

一个典型情况就是应用程序使用“服务端与客户端”的这种关系来管理进程通讯。所谓客户端就是指某个程序或者某个进程它从别的程序或者进程请求某一项服务。然后所谓的服务端他就是负责提供某项服务的程序或者进程。很多程序它既是客户端又是服务端,视情况而定。举个例子,一个文字处理程序它就必须扮演一个客户端的身份从作为服务器的电子表格应用程序请求制造成本汇总表。

当你真的觉得你有必要进行进程间通讯的时候,你就得选择一种方法用于进程间通讯。而具体使用哪种方法,取决于你的需求。因为不同的进程间通讯方法会带来不同的好处。
[*]你的程序它是不是真的有必要进行跨网络的进程间通讯?或者说它只要在一台机器上实现进程间通讯就够了?
[*]你的程序它是不是得和其它操作系统的进程进行通讯?(比如16位Windows或者UNIX)?
[*]你是不是需要用户去选择你的进程要和哪些进程通讯,或者你的程序它自己就能找到钦定的对象?
[*]你的程序是不是要和其它各种不同类型的程序进行通讯,比如复制粘贴数据到别的程序,或者你的程序有必要限制进程间通讯的操作种类?
[*]你的程序是不是对性能要求很严苛?所有的进程间通讯都是需要一些运行成本的。
[*]你的程序是一个有窗口的程序还是一个命令行程序?有些进程间通讯方法需要你有个窗口。


Windows支持以下的几种进程间通讯机制:
[*]剪贴板
[*]COM组件
[*]WM_COPYDATA窗口消息
[*]DDE(动态数据交换)
[*]文件映射
[*]Mailslots
[*]管道
[*]远程过程调用
[*]Windows Sockets


剪贴板方式进程间通讯
剪贴板充当应用程序间数据共享的中央存储库。当用户在应用程序中执行剪切或复制操作时,应用程序将选定的数据以一种或多种标准或应用程序定义的格式放在剪贴板上。然后,任何其他应用程序都可以从剪贴板中检索数据,并从可以理解的可用格式中进行选择。剪贴板是非常松散耦合的交换介质,应用程序只需要就数据格式达成一致。应用程序可以驻留在同一台计算机上或网络上的不同计算机上。

关键点:所有的程序都必须支持它们需要的数据的格式。举个例子,一个文本编辑器它就必须支持纯文本格式,能把纯文本复制进剪贴板,以及从剪贴板里面取出纯文本格式的数据。更多信息请参见剪贴板。

使用COM组件进行进程间通讯
使用OLE的应用程序管理复合文档 - 即由来自各种不同应用程序的数据组成的文档。OLE提供的服务使应用程序可以方便地调用其他应用程序进行数据编辑。例如,使用OLE的文字处理器可以嵌入电子表格中的图形。用户可以通过选择嵌入的图表进行编辑,从文字处理器内自动启动电子表格。OLE负责启动电子表格并显示图形进行编辑。当用户退出电子表格时,图表将在原始文字处理器文档中更新。电子表格似乎是字处理器的扩展。
OLE的基础是组件对象模型(COM)。使用COM的软件组件可以与各种各样的其他组件进行通信,甚至那些尚未写入的组件。这些组件作为对象和客户进行交互。分布式COM扩展了COM编程模型,使其可以在网络中工作。

关键点:OLE支持复合文档,并使应用程序能够包含嵌入的或链接的数据,在选择时自动启动另一个应用程序进行数据编辑。这使应用程序可以被任何其他使用OLE的应用程序扩展。COM对象通过一组或多组相关函数(称为接口)提供对对象数据的访问。有关更多信息,请参阅COM和ActiveX对象服务。

使用WM_COPYDATA窗口消息进行进程间通讯
这个方法需要发送方和接收方之间进行合作。接收方必须知道数据的格式并且能找出发送方是谁。重点是你不能一个疏忽就试图直接用指针来修改发送方的内存,也就是说你试图用这个消息传输指针的话,你要想操作这个指针指向的数据你就必须依赖ReadProcessMemory()或者WriteProcessMemory()而这需要你有权限打开目标进程——杀软大概就会觉得你是在搞事,于是把你当作病毒给你拦截了。而且这还需要解决UAC的问题。所以你只能用这个消息来传递实际的数据。

关键点:你需要一个窗口。此外,它比较快。其它的关键点我已经在上文说了。你必须知道你用它传输了啥,并且你要知道不同的进程之间的内存空间是不同的。它们不是一个次元的。所以传输指针的话,对方不能直接使用这个指针,除非在共享数据段。不然就GG。

使用DDE进行进程间通讯
DDE是一种使应用程序能够以各种格式交换数据的协议。应用程序可以使用DDE进行一次性数据交换,也可以使用DDE进行长期的数据交换,其中应用程序在新数据可用时相互更新。
DDE使用的数据格式与剪贴板使用的格式相同。DDE可以被认为是剪贴板机制的扩展。剪贴板几乎总是用于对用户命令的一次性响应,例如从菜单中选择“粘贴”命令。DDE通常也是由一个用户命令发起的,但是它通常在没有进一步用户交互的情况下继续运行。您还可以为通信要求更紧密耦合的应用程序之间的专用IPC定义自定义DDE数据格式。
DDE允许跨网络的数据交互。

关键点:DDE不如新技术高效。但是,如果其他IPC机制不合适,或者必须与仅支持DDE的现有应用程序接口,则仍然可以使用DDE。有关更多信息,请参阅动态数据交换和动态数据交换管理库。

使用文件映射进行进程间通讯
文件映射使得进程可以像处理进程的地址空间中的内存块一样处理文件的内容。该过程可以使用简单的指针操作来检查和修改文件的内容。当两个或多个进程访问相同的文件映射时,每个进程都会在自己的地址空间中接收一个指向内存的指针,以便读取或修改文件的内容。进程必须使用同步对象(如信号量)来防止多任务环境中的数据损坏。
您可以使用特殊情况下的文件映射来在进程之间提供命名的共享内存。如果在创建文件映射对象时指定了系统交换文件,则文件映射对象将被视为共享内存块。其他进程可以通过打开相同的文件映射对象来访问同一块内存。
文件映射非常高效,还提供了操作系统支持的安全属性,可以帮助防止未授权的数据损坏。文件映射只能在本地计算机上的进程之间使用;它不能通过网络使用。

关键点:文件映射是同一台计算机上的两个或更多进程共享数据的有效方式,但是您必须提供进程之间的同步。有关更多信息,请参阅文件映射和同步。

使用Mailslot进行进程间通讯
Mailslots提供单向沟通。任何创建Mailslots的过程都是Mailslots服务器。其他进程(称为Mailslots客户端)通过向其Mailslots写入消息来向邮件服务器发送消息。传入消息始终附加到Mailslots。Mailslots保存消息,直到Mailslots服务器读取它们。一个进程既可以是一个Mailslots服务器,也可以是一个Mailslots客户端,因此可以使用多个Mailslots进行双向通信。
Mailslots客户端可以将邮件发送到本地计算机上的Mailslots,另一台计算机上的Mailslots,或指定网络域中所有计算机上具有相同名称的所有Mailslots。广播到域上所有邮件槽的邮件长度不能超过400字节,而发送到单个Mailslots的邮件仅受到Mailslots服务器在创建Mailslots时指定的最大邮件大小的限制。

关键点:邮件区为应用程序发送和接收短消息提供了一个简单的方法。它们还提供了在网络域中的所有计算机上广播消息的能力。有关更多信息,请参阅Mailslots。

使用管道进行进程间通讯
有两种类型的双向通信管道:匿名管道和命名管道。匿名管道使相关进程能够相互传递信息。通常,匿名管道用于重定向子进程的标准输入或输出,以便它可以与其父进程交换数据。要双向交换数据(双工操作),您必须创建两个匿名管道。父进程使用其写入句柄将数据写入一个管道,而子进程使用其读取句柄从该管道读取数据。同样,子进程将数据写入另一个管道,父进程从中读取数据。匿名管道不能通过网络使用,也不能在不相关的进程之间使用。
命名管道用于在不相关进程的进程和不同计算机上的进程之间传输数据。通常,命名管道服务器进程会创建一个具有众所周知的名称或名称的命名管道,以传达给客户端。知道管道名称的命名管道客户端进程可以打开其另一端,受命名管道服务器进程指定的访问限制。服务器和客户端都连接到管道后,可以通过对管道进行读写操作来交换数据。

典型例子,匿名管道有仨:stdin,stdout和stderr。稍有C常识的人就知道,只要包含了stdio.h就能轻松使用匿名管道。然后printf函数默认是把东西输出到stdout,而perror则是输出东西到stderr。用scanf、getchar等函数可以读取stdin。用fopen、fread、fwrite、fprintf等可以操作各种管道。这种管道用于进程间通讯是十分方便的,而且就算你的程序是窗口程序而非控制台程序,你也是可以用管道的。

在Linux或者Unix,当你使用socket完成了一个TCPIP协议的连接后,你就能直接用read和write写管道的方式发送数据给远程目标了。贼方便。不过我还是习惯send和recv,因为在Windows下,socket不是文件描述符,而是“文件描述符”(注意双引号)。这是为了跨平台代码兼容。

关键点:匿名管道提供了一种有效的方法来将标准输入或输出重定向到同一台计算机上的子进程。 命名管道提供了一个简单的编程接口,用于在两个进程之间传输数据,不管它们驻留在同一台计算机上还是通过网络。 有关更多信息,请参阅管道。

使用远程过程调用(RPC)进行进程间通讯
RPC使应用程序可以远程调用功能。 因此,RPC使得IPC像调用函数一样简单。 RPC在单台计算机上的进程或网络上的不同计算机上运行。
Windows提供的RPC与开放软件基础(OSF)分布式计算环境(DCE)兼容。 这意味着使用RPC的应用程序能够与运行其他支持DCE的操作系统的应用程序进行通信。 RPC自动支持数据转换,以适应不同的硬件体系结构以及不同环境之间的字节顺序。
RPC客户端和服务器紧密耦合,但仍然保持高性能。该系统广泛使用RPC来促进操作系统不同部分之间的客户端/服务器关系。

这里我不得不说一句:某火极一时的比特币勒索病毒,也就是某香菇病毒(WannaCry)就是钻了RPC的漏洞钻进了受害者们的电脑。

关键点:RPC是一个功能级接口,支持自动数据转换和与其他操作系统的通信。 使用RPC,您可以创建高性能,紧密耦合的分布式应用程序。 有关更多信息,请参阅Microsoft RPC组件。

使用Windows Sockets进行进程间通讯
Windows套接字是一个协议无关的接口。它利用了底层协议的通信功能。在Windows套接字2中,套接字句柄可以选择用作具有标准文件I / O功能的文件句柄。
Windows套接字基于BSD首先推广的套接字。使用Windows套接字的应用程序可以与其他类型的系统上的其他套接字实现进行通信。但是,并非所有运输服务提供商都支持所有可用选项。

废话那么多总结一下就是:它和网络连接的概念差不多,但不一定走网络。然后“127.0.0.1”这个地址就是“这个包裹是发给自己机器的”的意思。经典的LAMP服务器模型里面php要查询mysql数据库的时候就是走的socket(虽然不是Windows Sockets而是Linux sockets)建立一个TCPIP连接,连接上mysqld进程,然后收发数据实现的进程间通讯。这玩意儿可以跨网传输。并且我比较喜欢这种方法。

关键点:Windows套接字是一个协议独立的接口,能够支持当前和新兴的网络功能。 有关更多信息,请参阅Windows套接字2。


YY菌 发表于 2024-2-18 00:07:50

本帖最后由 YY菌 于 2024-2-18 00:09 编辑

0xAA55 发表于 2024-2-8 00:47
最近也看了 IOCP,了解到了重叠(OVERLAPPED)的作用。当时想封装一个自己的跨平台 HTTP Server 来着,然 ...

我刚刚又发现一个骚操作能实现进程通信,就是直接使用 PostQueuedCompletionStatus 给IOCP发消息,这就可以不通过任何IO对象(包括文件、控制台、管道、socket等)实现更快的进程通信。CreateIoCompletionPort 可以创建一个IOCP队列,虽然这个API没有提供共享功能,但我们可以利用 DuplicateHandle 来跨进程复制句柄,有了这个骚操作就能实现 进程a 使用 PostQueuedCompletionStatus 给 进程b 发送任何自定义消息了。
虽然我们能用 DuplicateHandle 来让原本不支持共享的IOCP变得共享,但是要知道对方的IOCP句柄和进程id才能去复制到自己进程,怎么办呢?这个时候就需要用到另一个新的骚操作了 MapViewOfFile 来实现共享内存数据,把服务端的IOCP句柄和进程id放进去,这样客户端就可以轻松拿到服务端的数据来复制到客户端进程,这样客户端进程就可以随意通过 PostQueuedCompletionStatus 函数给服务端进程发送任何消息了。

下面给出测试代码的例子,先给共享代码的部分:
#pragma once

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

// 共享名称
EXTERN_C DECLSPEC_SELECTANY const TCHAR TestName[] = TEXT("TestProcessMessage{83dd28de-216c-4074-8983-f90d92cae09d}");

// 共享信息映射
struct SharedInfoMap
{
      volatile DWORD pid;
      volatile DWORD handle;
};

// 创建共享信息映射
static SharedInfoMap *CreateSharedInfoMap(LPCTSTR name, HANDLE &hMap)
{
      // 创建并映射跨进程共享内存段
      hMap = CreateFileMapping(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, sizeof(SharedInfoMap), name);
      if (!hMap) return nullptr;
      LPVOID pMap = MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, sizeof(SharedInfoMap));
      //CloseHandle(hMap);      // 注意:这里映射完后不能关闭句柄,否则其它进程就没法打开了。
      return static_cast<SharedInfoMap *>(pMap);
}

// 只读打开共享信息映射
static const SharedInfoMap *OpenSharedInfoMap(LPCTSTR name)
{
      // 打开并映射跨进程共享内存段
      HANDLE hMap = OpenFileMapping(FILE_MAP_READ, FALSE, name);
      if (!hMap) return nullptr;
      LPCVOID pMap = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, sizeof(SharedInfoMap));
      CloseHandle(hMap);      // 映射完了就可以关掉打开的句柄了
      return static_cast<const SharedInfoMap *>(pMap);
}

// 读写打开共享信息映射(正常情况下还是建议用只读方式打开不要修改服务端共享出来的内存)
static SharedInfoMap *OpenSharedInfoMapWriteable(LPCTSTR name)
{
      // 打开并映射跨进程共享内存段
      HANDLE hMap = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, name);
      if (!hMap) return nullptr;
      LPVOID pMap = MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, sizeof(SharedInfoMap));
      CloseHandle(hMap);      // 映射完了就可以关掉打开的句柄了
      return static_cast<SharedInfoMap *>(pMap);
}


然后是服务端的实现代码:
#include "共享代码.h"

static HANDLE hIocp;
static BOOL CALLBACK OnConCtrl(DWORD CtrlType);

int main()
{
      HANDLE hMap = nullptr;
      __try {
                // 创建IOCP对象
                if (hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1));
                else __leave;      // 失败就直接退出
                // 创建共享信息映射
                if (auto psim = CreateSharedInfoMap(TestName, hMap)) __try {
                        // 共享享进程id出去
                        psim->pid = GetCurrentProcessId();
                        // 共享享IOCP出去
                        psim->handle = DWORD(hIocp);
                }
                __finally {
                        // 共享完后就不需要这块内存的虚拟地址了,尽早释放为好(只要hMap在结束服务后释放就行了)。
                        UnmapViewOfFile(psim);
                }
                else __leave;      // 失败就直接退出
                // 注册控制台关闭事件处理
                SetConsoleCtrlHandler(OnConCtrl, TRUE);
                // 进入IOCP消息循环
                OVERLAPPED_ENTRY ole;
                DWORD num;
                // GetQueuedCompletionStatusEx 的用法可以理解为 GetMessage 和 PeekMessage。
                while (GetQueuedCompletionStatusEx(hIocp, &ole, 1, &num, INFINITE, FALSE)) {
                        // 如果是退出消息就返回传过来的退出码(可以理解为窗口消息的wParam参数)
                        if (WM_QUIT == ole.lpCompletionKey) return ole.dwNumberOfBytesTransferred;
                        // 其它的消息打印出来
                        printf("%x\t%p\t%lu\n", ole.lpCompletionKey, ole.lpOverlapped, ole.dwNumberOfBytesTransferred);
                }
      }
      __finally {
                // 退出前释放所有资源
                if (hMap) CloseHandle(hMap);
                if (hIocp) CloseHandle(hIocp);
      }
      // 失败退出时获取错误码来作为退出码
      return GetLastError();
}

// 控制台关闭事件响应
BOOL CALLBACK OnConCtrl(DWORD CtrlType)
{
      switch (CtrlType)
      {
      case CTRL_C_EVENT:
      case CTRL_BREAK_EVENT:
                // Ctrl+C 和 Ctrl+Break 退出时,只要给IOCP发送退出消息成功就阻止控制台杀掉自己进程和向父进程通知事件。
                return PostQueuedCompletionStatus(hIocp, EXIT_SUCCESS, WM_QUIT, nullptr);
      default:
                // 其它情况退出(如点击关闭按钮、系统注销或关机等)不管有没有给IOCP发送成功都不要阻止控制台对父进程的通知。
                if (!PostQueuedCompletionStatus(hIocp, EXIT_SUCCESS, WM_QUIT, nullptr)) break;
                // 需要做点操作来延迟一下返回的时间(否则一返回进程就会立即被控制台杀掉),尽量优先走IOCP的正常退出流程。
                SetConsoleCtrlHandler(OnConCtrl, FALSE);      // 取消再次控制台关闭事件的响应
                SwitchToThread();      // 放弃当前线程的CPU执行权,让别的线程(比如IOCP所在线程)优先执行。
      }
      // 返回FALSE保证控制台会继续通知到父进程。
      return FALSE;
}


最后是客户端的代码:
#include "共享代码.h"

int main()
{
      HANDLE hIocp = nullptr, hProcess = nullptr;
      __try {
                // 打开共享信息映射
                if (auto psim = OpenSharedInfoMap(TestName)) __try {
                        // 打开服务端进程并获取具有句柄复制和同步权限的进程句柄
                        if (hProcess = OpenProcess(PROCESS_DUP_HANDLE | SYNCHRONIZE, FALSE, psim->pid));
                        else __leave;      // 失败就直接退出
                        // 复制服务端的IOCP句柄到客户端进程
                        if (DuplicateHandle(hProcess, HANDLE(psim->handle), INVALID_HANDLE_VALUE, &hIocp, 0, FALSE, DUPLICATE_SAME_ACCESS));
                        else __leave;      // 失败就直接退出
                }
                __finally {
                        // 复制完后就不需要这块内存的虚拟地址了,尽早释放为好。
                        UnmapViewOfFile(psim);
                }
                else __leave;      // 失败就直接退出
                // 给服务端发送一条测试消息
                float x = 0.9f, y = 0.5f;
                if (PostQueuedCompletionStatus(hIocp, GetCurrentProcessId(), (int &)x - (int &)y, nullptr));
                else __leave;      // 失败就直接退出
                // 暂停控制台并等待按任意键继续
                if (IDYES == MessageBox(GetConsoleWindow(), TEXT("是否给服务端发送退出消息"), nullptr, MB_YESNO));
                else __leave;      // 取消也直接退出
                // 发送退出消息给服务端并等待服务端进程结束
                if (PostQueuedCompletionStatus(hIocp, EXIT_SUCCESS, WM_QUIT, nullptr))
                        WaitForSingleObject(hProcess, INFINITE);
      }
      __finally {
                // 退出前释放所有资源
                if (hProcess) CloseHandle(hProcess);
                if (hIocp) CloseHandle(hIocp);
      }
      // 失败退出时获取错误码来作为退出码
      return GetLastError();
}

liu496324 发表于 2023-7-6 08:36:47

楼主讲得很好,可惜不是很明白

YY菌 发表于 2024-1-22 15:08:06

因为在Windows下,socket不是文件描述符,而是“文件描述符”(注意双引号)。——这个说法是错误的,因为在Windows下,socket句柄仍然是真正的文件描述符,不然就不可能实现把socket绑定到IOCP的操作了,所以在Windows下仍然是支持的用ReadFile和WriteFile来读写socket数据的(以前在MSDN上看到过文档有明确说过socket对象支持ReadFile和WriteFile操作),只不过有一些限制很多人不知道而已。
Windows的IO对象分为重叠和非重叠两种模式(必须在创建句柄的时候指定),比如CreateFile是否指定FILE_FLAG_OVERLAPPED标记和WSASocket是否指定WSA_FLAG_OVERLAPPED标记来确定。而平常我们使用的跨平台版本socket函数是没法指定的,经过测试发现socket函数创建的句柄为重叠模式(相当于WSASocket函数的dwFlags参数指定WSA_FLAG_OVERLAPPED标记)。所以说socket句柄在Windows下默认为异步的重叠模式,在这个模式下使用非重叠的用法去调用ReadFile和WriteFile自然是会失败的。
经过测试:WSASocket创建非重叠句柄,使用非重叠阻塞ReadFile和WriteFile是完全OJBK的;而使用socket或WSASocket创建重叠句柄,使用重叠异步ReadFile和WriteFile也是OJBK的,但使用非重叠阻塞则会报无效参数错误。

0xAA55 发表于 2024-2-8 00:47:50

YY菌 发表于 2024-1-22 15:08
因为在Windows下,socket不是文件描述符,而是“文件描述符”(注意双引号)。——这个说法是错误的,因为 ...

最近也看了 IOCP,了解到了重叠(OVERLAPPED)的作用。当时想封装一个自己的跨平台 HTTP Server 来着,然后才了解到这个 OVERLAPPED 是用来做异步通信的。

lichao 发表于 2024-2-11 00:19:54

Socket最慢但也最通用, iOS上也有很多IPC通信方法但都有局限性,摸索到最后还是socket最稳,说慢吧本地环回地址访问还是极快的,没什么感知

0xAA55 发表于 2024-2-19 20:25:58

YY菌 发表于 2024-2-18 00:07
我刚刚又发现一个骚操作能实现进程通信,就是直接使用 PostQueuedCompletionStatus 给IOCP发消息,这就可 ...

还是你这个屌!
页: [1]
查看完整版本: 【翻译】MSDN资料:进程间通讯方法