【C++】DLL内共享数据区在进程间共享数据(重要)
因项目需要,需要在DLL中共享数据,即DLL中某一变量只执行一次,在运行DLL中其他函数时该变量值不改变;刚开始想法理解错误,搜到了DLL进程间共享数据段,后面发现直接在DLL中定义全局变量就行,当时脑袋有点犯2了。但既然接触到DLL进程间共享数据段,觉得还是挺重要的,干脆一不做二不休,就详细了解了下有关知识,进行了一些总结,留作备忘。
全局变量在DLL内使用,在同一进程同一DLL文件中的相互调用是正常的,包括指针的使用;不同进程中参数互不影响。
当C#启动后开始加载DLL文件,文件中的初始代码就会执行,所有全局变量会一直保存实值,直到C#程序运行结束或主动释放加载的DLL文件,这样DLL文件就可以被看作一个伴随C#主进程一直运行的子线程,运行过程中不会释放变量.
默认情况下,同一个程序启动多个进程,它们各自的变量值是不会相互影响的。第二个实例启动后,在修改全局变量的时候,系统会运用内存管理系统copy- on-write的特性来防止修改了第一个实例的数据,即系统会再分配一些内存,并将全局变量复制到这块内存中,每个实例使用自己的内存空间上的数据而互不影响。
下面重点介绍下DLL进程间共享数据段
在多个进程间共享数据,windows提供了这种方法,就是创建自己的共享数据节,并将需要共享的变量放入该内存中。如果是在相同程序的多个实例间共享数据,只要在exe文件创建共享节即可,否则就需要在DLL中创建共享节,其它进程加载该DLL来共享数据。
在Win32环境下要想在多个进程中共享数据,就必须进行必要的设置。在访问同一个Dll的各进程之间共享存储器是通过存储器映射文件技术实现的。也可以把这些需要共享的数据分离出来,放置在一个独立的数据段里,并把该段的属性设置为共享。必须给这些变量赋初值,否则编译器会把没有赋初始值的变量放在一个叫未被初始化的数据段中。
在DLL的实现文件中添加下列代码:
#pragma data_seg("Shared") // 声明共享数据段,并命名该数据段 int SharedData = 123; // 必须在定义的同时进行初始化!!!! #pragma data_seg()
在#pragma data_seg("DLLSharedSection")和#pragma data_seg()之间的所有变量将被访问该Dll的所有进程看到和共享。仅定义一个数据段还不能达到共享数据的目的,还要告诉编译器该段的属性,有三种方法可以实现该目的(其效果是相同的),一种方法是在.DEF文件中加入如下语句:
SETCTIONS
Shared READ WRITE SHARED
另一种方法是在项目设置的链接选项(Project Setting --〉Link)中加入如下语句:
/SECTION:Shared,rws
还有一种就是使用指令:
#pragma comment(linker,"/section:.Shared,rws") // 可读,可写,进程间共享。所有加载此dll的进程共享一份内存
那么这个数据节中的数据可以在所有DLL的实例之间共享了。所有对这些数据的操作都针对同一个实例的,而不是在每个进程的地址空间中都有一份。
当进程隐式或显式调用一个动态库里的函数时,系统都要把这个动态库映射到这个进程的虚拟地址空间里。这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈。
下面是几个需要注意的语法问题:
(1)#pragma data_seg()一般用于DLL中。也就是说,在DLL中定义一个共享的,有名字的数据段。最关键的是:这个数据段中的全局变量可以被多个进程共享。否则多个进程之间无法共享DLL中的全局变量.
(2)数据段的名称为“Shared”,那么在设置该段属性的时候,一定要保证段名称完全与“Shared”相同,而且大小写敏感。一旦两者不同,连接器会警告错误。 >LINK : warning LNK4039: 用 /SECTION 选项指定的节“Shared”不存在。注意是警告错误,所以DLL文件会继续编译连接成功,只是Shared数据段并没有设置为共享段。
(3)最后一行中的rws之前不能有空格,否则编译器报错。 1>main.obj : fatal error LNK1276: 找到无效的指令“rws”; 未以“/”开头。然后停止编译连接。
(4)共享段中的变量一定要初始化,否则连接器也会报错,也不能正常设置为共享段。 所有在共享数据段中的变量,只有在数据段中经过了初始化之后,才会是进程间共享的。如果没有初始化,那么进程间访问该变量则是未定义的,编译器会把没有初始化的数据放到.BSS段中,从而导致多个进程之间的共享行为失败。 1>LINK : warning LNK4039: 用 /SECTION 选项指定的节“Shared”不存在。 但是继续生成dll文件。
这几种错误,最严重的就是(2)和(3),因为虽然没有成功设置共享段,但是仍然编译成功,稍不注意,就会非常危险。对于(4)则根本不能编译成功,所以只要了解语法修改就可以了,不存在潜在危险。
(5) 所有的共享变量都要放置在共享数据段中。如果定义很大的数组,那么也会导致很大的DLL。 (6)不要在共享数据段中存放进程相关的信息。Win32中大多数的数据结构和值(比如HANDLE)只在特定的进程上下文中才是有效地。 (7)每个进程都有它自己的地址空间。因此不要在共享数据段中共享指针,指针指向的地址在不同的地址空间中是不一样的。 (8)DLL在每个进程中是被映射在不同的虚拟地址空间中的,因此函数指针也是不安全的。 (9)当然还有其它的方法来进行进程间的数据共享,比如文件内存映射等,涉及到通用的进程间通信。
特别注意的是:(特别重要)
Dll中共享数据的限制: 1、必须初始化共享数据段中的所有变量。 2、所有共享变量都存放在编译DLL的指定数据段中。 3、永远不要将特定于进程的信息存储在共享数据段中。 4、永远不要将指针存储在共享段包含的变量中。 5、具有虚拟函数的类总是包含函数指针。因此有虚拟函数的类永远不要存储在共享数据段中。 6、静态数据成员以全局变量的等效形式实现。因此每个进程都有他自己的该静态数据成员的副本。不应存储具有静态数据成员的类。 7、对于 C++ 类,共享数据段的初始化要求引起一个特定问题。如果共享数据段中有类似 CTest Counter(0); 的内容,则当每个进程加载 DLL 时,Counter 对象将在该进程中初始化,
从而有可能每次都将对象的数据清零。这与内部数据类型(由链接器在创建 DLL 时初始化)非常不同。 因此不建议在进程间共享C++对象。
实例测试
在这里,用C++封装DLL,用WPF工程来测试,由于一般项目都是单进程的,所以我们创建两个WPF工程当做两个进程进行测试。
C++封装代码如下:
testdll.h文件
#ifndef TestDll_H_ #define TestDll_H_ #ifdef MYLIBDLL #define MYLIBDLL extern "C" _declspec(dllimport) #else #define MYLIBDLL extern "C" _declspec(dllexport) #endif
//可以include需要用到的头文件
MYLIBDLL void SetData(int num1, int num2); MYLIBDLL int GetArray(); MYLIBDLL int getNum(); MYLIBDLL int getInfoAge(); #endif
testdll.cpp文件
#include "testdll.h" #include <iostream> using namespace std;
struct Info { int num; int age; }; ////////////////////////////////////////////// 进程共享区 /////////////////////////////////////// #pragma data_seg("Shared") // 声明共享数据段,并命名该数据段 Info info = {1}; // 变量必须在定义的同时进行初始化!!!! int p[2] ={1,2}; //不能用指针 int num = 0; #pragma data_seg() #pragma comment(linker, "/section:Shared,rws") // 可读,可写,进程间共享。所有加载此dll的进程共享一份内存 void SetData(int num1, int num2) { num = num1 + num2; info.age = num2; p[0] = num1; } int GetArray() { return p[0]; } int getNum() { return num; } int getInfoAge() { return info.age; }
WPF工程引用DLL的接口类:
class Test {
//相对路径
//[DllImport(@"..\\..\\..\\..\\DLL\\Win32Project1.dll", EntryPoint = "SetData", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] [DllImport(@"D:\\DLL\\Win32Project1.dll", EntryPoint = "SetData", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern void SetData(int m, int n); [DllImport(@"D:\\DLL\\Win32Project1.dll", EntryPoint = "GetArray", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int GetArray(); [DllImport(@"D:\\DLL\\Win32Project1.dll", EntryPoint = "getNum", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int getNum(); [DllImport(@"D:\\DLL\\Win32Project1.dll", EntryPoint = "getInfoAge", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] public static extern int getInfoAge(); }
WPF工程1:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //第一次执行,启动工程1 Test.SetData(5, 6); int a = Test.GetArray(); //5 int b = Test.getNum(); //11 int c = Test.getInfoAge(); //6 } private void Button_Click(object sender, RoutedEventArgs e) { //第四次执行,点击按钮 int a = Test.GetArray(); //11 int b = Test.getNum(); //24 int c = Test.getInfoAge(); //13 } }
WPF工程2
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //第二次执行,启动工程2 int a = Test.GetArray(); //5 int b = Test.getNum(); //11 int c = Test.getInfoAge(); //6 } private void Button_Click(object sender, RoutedEventArgs e) { //第三次执行,点击按钮 Test.SetData(11, 13); int a = Test.GetArray(); //11 int b = Test.getNum(); //24 int c = Test.getInfoAge(); //13 } }
上述第N次执行为2个程序运行顺序,可以看到两者能进行进程间共享数据,
在这里需要特别提醒的是,两个工程引用的DLL必须是同一个,DLL复制一份,就相当于不是同一个dll了。
这里发现一个问题:(共享结构体、类时发生)
struct Info{ int num; int age; }; struct Info2{ int num; int age; Info2(int a){ num = a; age = a; } }; struct Info3{ int num; int age; Info3(int a){ } };
当我C++封装DLL时结构体分别为上述三种类型,我们会发现在第二次执行时,int c = Test.getInfoAge();的值会有不同,Info和Info3为正常的6,Info会为1。
在这里分析原因可能如下:(注意这里可能是由于调用类的构造函数中初始化赋值操作才导致的)
对于 C++ 类,共享数据段的初始化要求引起一个特定问题。如果共享数据段中有类似 CTest Counter(0); 的内容,则当每个进程加载 DLL 时,Counter 对象将在该进程中初始化,从而有可能每次都将对象的数据清零。这与内部数据类型(由链接器在创建 DLL 时初始化)非常不同。 因此不建议在进程间共享C++对象。
自己理解的:
即对象类的共享数据(结构体、类等)在每个进程加载DLL时都会执行初始化操作,一般是调用构造函数。如果调用默认构造函数 Info(){}; 因为没有对变量进行赋值,所以第二个进程启动时
不会影响第一次的执行结果,Info3同理;而对于Info2,在构造函数中对变量进行了赋值操作,因此会重写共享数据段的数据,造成错误结果。
参考链接:http://www.cnblogs.com/bjguanmu/articles/4398121.html
http://blog.csdn.net/chinabinlang/article/details/17751601
http://blog.csdn.net/trustbo/article/details/11937211