【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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM