进程间通信 - 动态链接库实现

时间:2022-11-08 15:47:55

引子

前面介绍的几种用于实现进程之间通信的方法都是比较正统的方式,

像剪贴板,命名管道这些都还是用得比较多的,

而这里将介绍的是一种比较偏门的方法来实现进程间的通信,

所谓偏门呢,自然就是用的少,能够不用就不要使用。

其实这种方法就是通过动态链接库来实现进程间的通信。

          

                

动态链接库(DLL)概述              

既然是要通过动态链接库来实现进程间的通信,

那么这里如果不来介绍一下动态链接库的话,怎么也说不过去的。

动态链接库是 Windows 操作系统的基础,其中 Windows API 基本上都是以动态链接库的形式来提供的,

通常来说动态链接库是不能够直接运行,也不能够直接接受消息的,

它们是一些独立的文件(后缀名一般为 .dll ,当然还有其他的一些后缀名也是可以的),

其中包含能被可执行程序或其他 DLL 调用来完成某项工作的函数,

也就是说动态链接库也就是由一些函数组成而已。

并且只有在其他模块调用动态链接库中的函数时,动态链接库才发挥作用,

在实际的编程中,通常可以把完成某种功能的函数放在一个动态链接库中,然后提供给其他函数调用。

当这个访问了动态链接库的进程被加载时,系统会为这个进程分配 4GB 的私有地址空间,

然后系统就会分析这个可执行模块,找到这个可执行模块中所调用的 DLL ,然后系统就负责搜索这些 DLL 

找到这些 DLL 后便将这些 DLL 加载到内存中,并为它们分配虚拟的内存空间,

最后将 DLL 的页面映射到调用进程的地址空间中,

DLL 的虚拟内存有代码页和数据页,它们被分别映射到 进程 A 的代码页面和数据页面,

如果这时 进程 B 也启动了,并且 进程 B 也需要访问该 DLL 

这时,只需要将该 DLL 在虚拟内存中的代码页面和数据页面映射到第二个进程的地址空间即可。

这也表明了在内存中,只需要存在一份 DLL 的代码和数据,

多个进程共享 DLL 的同一份代码,很明显这样做可以节省内存空间的。

但是在 Windows 下,由于系统会为每一个进程分配 4GB 的私有地址空间,

而 DLL 中的代码和数据也只是映射到了这个私有地址空间中,所以这些应用程序之间还是不能够相互影响的,

也就是说多个应用程序虽然是可以共享同一个 DLL 中的相同的代码的,

但是 DLL 为每一个进程保存的数据都是不相同的,

并且每一个进程都为 DLL 使用的全部数据分配了自己的地址空间,

举个最简单的例子,我的 DLL 中有一个函数为 int   Add(int    num1 ,   int    num2)

这个函数的作用是实现 num1  和 num2 相加并返回相加后的结果。

然后我有一个 进程 A  使用了这个 DLL ,并且其调用了函数  Add(10, 20),

然后我还有一个 进程 B 其也使用了这个 DLL ,并且其调用了函数 Add(30, 40),

那么对于 进程 A 中的数据 10 和 20 其实是保存在 进程 A 的私有地址空间中的,

而对于 进程 B 中的数据 30 和 40 则是保存在 进程 B 的私有地址空间中的,

上面这个简单的例子表明如果单单用这种简单的使用动态链接库的方式是不能够实现进程之间的通信的。

      

          

动态链接库*享内存的实现

如果想利用动态链接库来实现进程间的通信的话,那么有一种方案可以试一试,

即从系统为动态链接库分配的那一块内存(系统需要将动态链接库加载到内存中)下手,

由于在内存中,动态链接库其实只存在一份,

其被所有需要调用该动态链接库中的函数的模块或者简单说是可执行程序所共享,

既然是共享的话,如果我在系统给动态链接库分配的这块内存中保存数据,

那岂不是可以被所有访问该动态链接库的可执行程序所获取或者说设置。

这样的话,我就可以使用 进程 A 来设置好这个共享内存中的数据,

然后 进程 B 就可以读取这个共享内存中的数据了,这不是也可以实现进程间的通信嘛,

这样看来的话,其思路其实和使用剪贴板是一模一样的了。

也是采用一块两个进程共享的内存来作为存放数据的中介。

                  

          

示例:动态链接库实现进程间通信

共享 DLL 实现:

新建动态链接库项目步骤:

进程间通信 - 动态链接库实现

进程间通信 - 动态链接库实现

项目结构:

进程间通信 - 动态链接库实现

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif

         

DLL.cpp

// DLL.cpp : 定义 DLL 应用程序的导出函数。
//
 
#include "stdafx.h"
 
#define DLL_API extern "C" _declspec(dllexport)
 
#include "ShareDLL.h"
 
//使用 #pragma data_seg() 来表明这一段数据为共享数据
//一定要注意给下面的变量初始化,否则将无法实现数据在多个进程间共 享
#pragma data_seg("SharedDataInDll")
 
    //初始化为 0
    int data = 0;
 
#pragma data_seg()
 
 
//这里还需要告诉链接器表明 SharedDataInDll 数据段为可读可写可共享
#pragma comment(linker, "/SECTION:SharedDataInDll,RWS")
 
 
//返回共享数据
int GetData()
{
    return data;
}
 
//设置共享数据
void SetData(int tmpData)
{
    data = tmpData;
}

进程 A 实现:(简单 Console 程序)

项目结构:

进程间通信 - 动态链接库实现

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif
               

DLLProcessA.cpp

#include <iostream>
#include "ShareDLL.h"
 
using namespace std;
 
//引用 DLL.lib 引入库
#pragma comment(lib, "DLL.lib")
 
int main(int argc, char * argv)
{
    int data;
 
    cout<<"进程 A 设置数据:  ";
    cin>>data;
 
    //设置共享内存
    SetData(data);
 
    cout<<endl<<endl;
    system("pause");
 
    //读取共享内存
    cout<<"进程 A 读取数据:  "<<GetData()<<endl<<endl;
 
    system("pause");
}

进程 B 实现:(简单 Console 程序)

项目结构:

进程间通信 - 动态链接库实现

ShareDLL.h

#ifndef SHARED_DLL
#define SHARED_DLL
 
//在 DLL 项目中设置 DLL_API 为导出类型 extern "C" _declspec(dllimport)
//在 Test 项目中则无需设置该 DLL_API , 直接使用这个 CalculateDLL.h 文件即可
 
#ifdef DLL_API
#else 
    #define DLL_API extern "C" _declspec(dllimport)
#endif
 
DLL_API void SetData(int tmpData);
DLL_API int GetData();
 
#endif
          

DLLProcessB.cpp

#include <iostream>
#include "ShareDLL.h"
 
using namespace std;
 
//引用 DLL.lib 引入库
#pragma comment(lib, "DLL.lib")
 
int main(int argc, char * argv)
{
    int data;
 
    //读取共享数据
    cout<<"进程 B 读取数据:  "<<GetData()<<endl<<endl;
 
    cout<<"进程 B 设置数据:  ";
    cin>>data;
 
    //设置共享数据
    SetData(data);
 
    cout<<endl<<endl;
    system("pause");
}

需要将 DLL 项目中的 DLL . dll 和 DLL . lib 两个文件,

分别拷贝到项目 DLLProcessA 和 DLLProcessB 的根目录下。

然后分别编译 DLLProcessA 和 DLLProcessB 两个项目,

最后将 Dll . dll 和 DLL . lib 以及 DLLProcesA . exe 和 DLLProcessB . exe 拷贝到同一目录下面,

比如:(这样可以确保两个进程访问到的是同一个动态链接库)

进程间通信 - 动态链接库实现

效果展示:

首先运行 DLLProcessA . exe 文件并设置共享数据为 8 :

进程间通信 - 动态链接库实现

然后启动 DLLProcessB . exe 文件(可以看出其读出的值为 8 ):

进程间通信 - 动态链接库实现

然后再在 DLLProcessB . exe 中设置数据为 16 :

进程间通信 - 动态链接库实现

然后再在 DLLProcessA . exe 中按下回车键(此时可以看到进程 A 读取到的数据位 16 了):

进程间通信 - 动态链接库实现

           

          

结束语

从上面的这个效果展示中可以看出,我们确实通过动态链接库实现了 进程 A 和 进程 B 之间的通信,

前面说过使用动态链接库来实现进程之间的通信是一个偏方,

通过这个 Demo 呢,我们也是可以看出这种方式的局限性的,

第一,使用动态链接库来实现进程间的通信的话,首先必须要求通信的双方进程都访问了这个动态链接库。

第二,这种方式只适用于本地进程之间的通信,其不能实现跨网络的通信。

关于进程之间通信呢,前前后后介绍了五种方法,其中各有各的优点,

也各有各的局限性,至于具体要使用那一种的话,那还请各位看官自行斟酌,然后选用合适的方案 !!!