如何在程式間共享資料

whatday發表於2013-06-24

1、引言
  在Windows程式中,各個程式之間常常需要交換資料,進行資料通訊。WIN32 API提供了許多函式使我們能夠方便高效的進行程式間的通訊,通過這些函式我們可以控制不同程式間的資料交換,就如同在WIN16中對本地程式進行讀寫操作一樣。
  典型的WIN16兩程式可以通過共享記憶體來進行資料交換:(1)程式A將GlobalAlloc(GMEM_SHARE...)API分配一定長度的記憶體;(2)程式A將GlobalAlloc函式返回的控制程式碼傳遞給程式B(通過一個登入訊息);(3)程式B對這個控制程式碼呼叫GlobalLock函式,並利用GlobalLock函式返回的指標訪問資料。這種方法在WIN32中可能失敗,這是因為GlobalLock函式返回指向的是程式A的記憶體,由於程式使用的是虛擬地址而非實際實體地址,因此這一指標僅與A程式有關,而於B程式無關。
  本文探討了幾種WIN32下程式之間通訊的幾種實現方法,讀者可以使用不同的方法以達到程式執行高效可靠的目的。

2、Windows95中程式的記憶體空間管理
  WIN32程式間通訊與Windows95的記憶體管理有密切關係,理解Windows95的記憶體管理對我們如下的程式設計將會有很大的幫助,下面我們討論以下Windows95中程式的記憶體空間管理。
  在WIN16下,所有Windows應用程式共享單一地址,任何程式都能夠對這一空間中屬於共享單一的地址空間,任何程式都能夠對這一空間中屬於其他程式的記憶體進行讀寫操作,甚至可以存取作業系統本身的資料,這樣就可能破壞其他程式的資料段程式碼。
  在WIN32下,每個程式都有自己的地址空間,一個WIN32程式不能存取另一個地址的私有資料,兩個程式可以用具有相同值的指標定址,但所讀寫的只是它們各自的資料,這樣就減少了程式之間的相互干擾。另一方面,每個WIN32程式擁有4GB的地址空間,但並不代表它真正擁有4GB的實際實體記憶體,而只是作業系統利用CPU的記憶體分配功能提供的虛擬地址空間。在一般情況下,絕大多數虛擬地址並沒有實體記憶體於它對應,在真正可以使用這些地址空間之前,還要由作業系統提供實際的實體記憶體(這個過程叫"提交"commit)。在不同的情況下,系統提交的實體記憶體是不同的,可能是RAM,也可能是硬碟模擬的虛擬記憶體。

3、WIN32中程式間的通訊
  在Windows 95中,為實現程式間平等的資料交換,使用者可以有如下幾種選擇:
  * 使用記憶體對映檔案
  * 通過共享記憶體DLL共享記憶體
  * 向另一程式傳送WM_COPYDATA訊息
  * 呼叫ReadProcessMemory以及WriteProcessMemory函式,使用者可以傳送由GlobalLock(GMEM_SHARE,...)函式呼叫提取的控制程式碼、GlobalLock函式返回的指標以及VirtualAlloc函式返回的指標。

3.1、利用記憶體對映檔案實現WIN32程式間的通訊
  Windows95中的記憶體對映檔案的機制為我們高效地操作檔案提供了一種途徑,它允許我們在WIN32程式中保留一段記憶體區域,把目標檔案對映到這段虛擬記憶體中。在程式實現中必須考慮各程式之間的同步。具體實現步驟如下:
首先我們在傳送資料的程式中需要通過呼叫記憶體對映API函式CreateFileMapping建立一個有名的共享記憶體:
HANDLE CreateFileMapping(
HANDLE hFile, // 對映檔案的控制程式碼,
//設為0xFFFFFFFF以建立一個程式間共享的物件
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全屬性
DWORD flProtect, // 保護方式
DWORD dwMaximumSizeHigh, //物件的大小
DWORD dwMaximumSizeLow,
LPCTSTR lpName // 必須為對映檔案命名
);

  與虛擬記憶體類似,保護方式可以是PAGE_READONLY或是PAGE_READWRITE。如果多程式都對同一共享記憶體進行寫訪問,則必須保持相互間同步。對映檔案還可以指定PAGE_WRITECOPY標誌,可以保證其原始資料不會遭到破壞,同時允許其他程式在必要時自由的運算元據的拷貝。
  在建立檔案對映物件後使用可以呼叫MapViewOfFile函式對映到本程式的地址空間內。
  下面說明建立一個名為MySharedMem的長度為4096位元組的有名對映檔案:
  HANDLE hMySharedMapFile=CreateFileMapping((HANDLE)0xFFFFFFFF),
  NULL,PAGE_READWRITE,0,0x1000,"MySharedMem");
  並對映快取區檢視:
  LPSTR pszMySharedMapView=(LPSTR)MapViewOfFile(hMySharedMapFile,
  FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);

  其他程式訪問共享物件,需要獲得物件名並呼叫OpenFileMapping函式。
  HANDLE hMySharedMapFile=OpenFileMapping(FILE_MAP_WRITE,
  FALSE,"MySharedMem");

  一旦其他程式獲得對映物件的控制程式碼,可以象建立程式那樣呼叫MapViewOfFile函式來對映物件檢視。使用者可以使用該物件檢視來進行資料讀寫操作,以達到資料通訊的目的。

  當使用者程式結束使用共享記憶體後,呼叫UnmapViewOfFile函式以取消其地址空間內的檢視:
if (!UnmapViewOfFile(pszMySharedMapView))
{ AfxMessageBox("could not unmap view of file"); }

3.2、利用共享記憶體DLL
  共享資料DLL允許程式以類似於Windows 3.1 DLL共享資料的方式訪問讀寫資料,多個程式都可以對該共享資料DLL進行資料操作,達到共享資料的目的。在WIN32中為建立共享記憶體,必須執行以下步驟:
  首先建立一個有名的資料區。這在Visual C++中是使用data_seg pragma巨集。使用data_seg pragma巨集必須注意資料的初始化:
#pragma data_seg("MYSEC")
char MySharedData[4096]={0};
#pragma data_seg()
然後在使用者的DEF檔案中為有名的資料區設定共享屬性。
LIBRARY TEST
DATA READ WRITE
SECTIONS
.MYSEC READ WRITE SHARED

  這樣每個附屬於DLL的程式都將接受到屬於自己的資料拷貝,一個程式的資料變化並不會反映到其他程式的資料中。

  在DEF檔案中適當地輸出資料。以下的DEF檔案項說明了如何以常數變數的形式輸出MySharedData。
EXPORTS
MySharedData CONSTANT
最後在應用程式(程式)按外部變數引用共享資料。
extern _export"C"{char * MySharedData[];}
程式中使用該變數應注意間接引用。
m_pStatic=(CEdit*)GetDlgItem(IDC_SHARED);
m_pStatic->GetLine(0,*MySharedData,80);
3.3、用於傳輸只讀資料的WM_COPYDATA
  傳輸只讀資料可以使用Win32中的WM_COPYDATA訊息。該訊息的主要目的是允許在程式間傳遞只讀資料。     Windows95在通過WM_COPYDATA訊息傳遞期間,不提供繼承同步方式。SDK文件推薦使用者使用SendMessage函式,接受方在資料拷貝完成前不返回,這樣傳送方就不可能刪除和修改資料:

SendMessage(hwnd,WM_COPYDATA,wParam,lParam);
其中wParam設定為包含資料的視窗的控制程式碼。lParam指向一個COPYDATASTRUCT的結構:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;//使用者定義資料
DWORD cbData;//資料大小
PVOID lpData;//指向資料的指標
}COPYDATASTRUCT;
該結構用來定義使用者資料。

3.4、直接呼叫ReadProcessMemory和WriteProcessMemory函式實現程式間通訊
通過呼叫ReadProcessMemory以及WriteProcessMemory函式使用者可以按類似與Windows3.1的方法實現程式間通訊,在傳送程式中分配一塊記憶體存放資料,可以呼叫GlobalAlloc或者VirtualAlloc函式實現:
pApp->m_hGlobalHandle=GlobalAlloc(GMEM_SHARE,1024);
可以得到指標地址:
pApp->mpszGlobalHandlePtr=(LPSTR)GlobalLock
(pApp->m_hGlobalHandle);
在接收程式中要用到使用者希望影響的程式的開啟控制程式碼。為了讀寫另一程式,應按如下方式呼叫OpenProcess函式:
HANDLE hTargetProcess=OpenProcess(
STANDARD_RIGHTS_REQUIRED|
PROCESS_VM_REDA|
PROCESS_VM_WRITE|
PROCESS_VM_OPERATION,//訪問許可權
FALSE,//繼承關係
dwProcessID);//程式ID
為保證OpenProcess函式呼叫成功,使用者所影響的程式必須由上述標誌建立。
一旦使用者獲得一個程式的有效控制程式碼,就可以呼叫ReadProcessMemory函式讀取該程式的記憶體:
BOOL ReadProcessMemory(
HANDLE hProcess, // 程式指標
LPCVOID lpBaseAddress, // 資料塊的首地址
LPVOID lpBuffer, // 讀取資料所需緩衝區
DWORD cbRead, // 要讀取的位元組數
LPDWORD lpNumberOfBytesRead
);
使用同樣的控制程式碼也可以寫入該程式的記憶體:
BOOL WriteProcessMemory(
HANDLE hProcess, // 程式指標
LPVOID lpBaseAddress, // 要寫入的首地址
LPVOID lpBuffer, // 緩衝區地址
DWORD cbWrite, // 要寫的位元組數
LPDWORD lpNumberOfBytesWritten
);
如下所示是讀寫另一程式的共享記憶體中的資料:
ReadProcessMemory((HANDLE)hTargetProcess,
(LPSTR)lpsz,m_strGlobal.GetBuffer(_MAX_FIELD),
_MAX_FIELD,&cb);
WriteProcessMemory((HANDLE)hTargetProcess,
(LPSTR)lpsz,(LPSTR)STARS,
m_strGlobal.GetLength(),&cb);

4、程式之間的訊息傳送與接收
  在實際應用中程式之間需要傳送和接收Windows訊息來通知程式間相互通訊,傳送方傳送通訊的訊息以通知接收方,接收方在收到傳送方的訊息後就可以對記憶體進行讀寫操作。
  我們在程式設計中採用Windows註冊訊息進行訊息傳遞,首先在傳送程式初始化過程中進行訊息註冊:
m_nMsgMapped=::RegisterWindowsMessage("Mapped");
m_nMsgHandle=::RegisterWindowsMessage("Handle");
m_nMsgShared=::RegisterWindowsMessage("Shared");
在程式執行中向接收程式傳送訊息:
CWnd* pWndRecv=FindWindow(lpClassName,"Receive");
pWndRecv->SendMessage(m_MsgMapped,0,0);
pWndRecv->SendMessage(m_nMsgHandle,
(UINT)GetCurrentProcessID(),(LONG)pApp->m_hGlobalHandle);
pWndRecv->SendMessage(m_nMsgShared,0,0);
可以按如下方式傳送WM_COPYDATA訊息:
static COPYDATASTRUCT cds;//使用者存放資料
pWnd->SendMessage(WM_COPYDATA,NULL,(LONG)&cds);

接收方程式初始化也必須進行訊息註冊:

UNIT CRecvApp:: m_nMsgMapped=::RegisterWindowsMessage("Mapped");
UNIT CRecvApp::m_nMsgHandle=::RegisterWindowsMessage("Handle");
UNIT CRecvApp::m_nMsgShared=::RegisterWindowsMessage("Shared");
同時對映訊息函式如下:
ON_REGISTERED_MASSAGE(CRecvApp::m_nMsgMapped,OnRegMsgMapped)
ON_REGISTERED_MASSAGE(CRecvApp::m_nMsgHandle,OnRegMsgHandle)
ON_REGISTERED_MASSAGE(CRecvApp::m_nMsgShared,OnRegMsgShared)
在這些訊息函式我們就可以採用上述技術實現接收程式中資料的讀寫操作了。

5、結束語

  從以上分析中我們可以看出Windows95的記憶體管理與Windows 3.x相比有很多的不同,對程式之間的通訊有較為嚴格的限制。這就確保了任何故障程式無法意外地寫入使用者的地址空間,而使用者則可根據實際情況靈活地進行程式間的資料通訊,從這一點上來講Windows95增強應用程式的強壯性。

相關文章