天天看點

共享記憶體實作程序間大資料的交換

2003-08-11 11:21作者:中國電波傳播研究所青島分所郎銳出處:yesky責任編輯:方舟

原文位址:http://www.yesky.com/42/1720542.shtml

  引言

  程序間的資料交換和共享是一種非常重要和實用的技術。大、中型軟體的開發設計多是由衆多程式設計人員的合作完成,通常一個程式設計人員隻負責其中一個或幾個子產品的開發,這些子產品可以是動态連結庫也可以是應用程式或是其他形式的程式元件。這些獨立開發出來的程式子產品最終需要作為一個整體來運作,即組成一個系統,在系統運作期間這些子產品往往需要頻繁地進行資料交換和資料共享,對于動态連結庫同其主調應用程式之間的資料交換是非常容易實作的,但是在兩個應用程式之間或是動态連結庫同其主調應用程式之外的其他應用程式進行資料交換就比較困難了。尤其是在交換資料量過大、交換過于頻繁的情況下更是難以實作,本文即對此展開讨論,并提出了一種通過共享記憶體來實作程序見大資料量快速交換的一種方法。

  通訊方式的比較和選擇

  程序間通訊的方式有很多,常用的有共享記憶體、命名管道和匿名管道、發送消息等幾種方法來直接完成,另外還可以通過socket口、配置檔案和系統資料庫等來間接實作程序間資料通訊任務。以上這幾種方法各有優缺點,具體到在程序間進行大資料量資料的快速交換問題上,則可以排除使用配置檔案和系統資料庫的方法;另外,由于管道和socket套接字的使用需要有網卡的支援,是以也可以不予考慮。這樣,可供選擇的通訊方式隻剩下共享記憶體和發送消息兩種。由于資料量比較大,這樣在使用消息進行通訊時就無法通過消息參數将資料直接攜帶到接收方,隻能以位址傳送的方式進行。當一個應用程式向另一個應用程式發送資料時将會發出WM_COPYDATA系統消息,是以可以考慮通過向消息隊列插入WM_COPYDATA消息的方法來實作資料在程序間的拷貝。

  在使用WM_COPYDATA消息時,由第一個消息參數指定發送視窗的句柄,第二個消息參數則為一同資料相關的資料結構COPYDATASTRUCT的指針,此結構原形聲明如下:

typedef struct tagCOPYDATASTRUCT {

DWORD dwData; 

DWORD cbData; 

PVOID lpData; 

} COPYDATASTRUCT;

  其中,隻需将待發送資料的首位址賦予lpData、并由cbData指明資料塊長度即可。消息發出後,接收方程式在WM_COPYDATA消息的響應函數中通過随消息傳遞進來的第二個參數完成對資料塊的接收。但是在使用WM_COPYDATA消息時,隻能用SendMessage()函數發送而不能使用PostMessage(),這兩個函數雖然功能非常相似都是負責向指定的視窗發送消息,但是SendMessage()函數發出消息後不是馬上傳回,而是在接收方的消息響應函數處理完之後才能傳回,并能夠得到傳回結果。在此期間發送方程式将被阻塞,SendMessage()後面的語句不能被繼續執行。而PostMessage()函數在發出消息後馬上傳回,其後語句能夠被立即執行,但是無法擷取消息的執行結果。可見,在交換資料量較大的情況下實作資料頻繁而又快速的交換用發送WM_COPYDATA消息的方法也是不合适的,當資料傳輸過于頻繁時将有可能導緻資料的丢失。

  比之以上幾種程序間通訊方法,共享記憶體有着明顯的優勢。共享記憶體是通過直接操作記憶體映射檔案來進行的,而記憶體映射檔案又是進行單機資料共享的最低層機制,前面幾種資料交換方式在低層都是通過記憶體映射檔案來進行的。是以使用共享記憶體可以以較小的開銷擷取較高的性能,是進行大資料量資料快速交換的最佳方案。

  共享記憶體的使用

  在Windows作業系統下,任何一個程序不允許讀取、寫入或是修改另一個程序的資料(包括變量、對象和記憶體配置設定等),但是在某個程序内建立的檔案映射對象的視圖卻能夠為多個其他程序所映射,這些程序共享的是實體存儲器的同一個頁面。是以,當一個程序将資料寫入此共享檔案映射對象的視圖時,其他程序可以立即擷取資料變更情況。為了進一步提高資料交換的速度,還可以采用由系統頁檔案支援的記憶體映射檔案而直接在記憶體區域使用,顯然這種共享記憶體的方式是完全可以滿足在程序間進行大資料量資料快速傳輸任務要求的。下面給出在兩個互相獨立的程序間通過檔案映射對象來配置設定和通路同一個共享記憶體塊的應用執行個體。在本例中,由發送方程式負責向接收方程式發送資料,檔案映射對象由發送方建立和關閉,并且指定一個唯一的名字供接收程式使用。接收方程式直接通過這個唯一指定的名字打開此檔案映射對象,并完成對資料的接收。

  在發送方程式中,首先通過CreateFileMapping()函數建立一個記憶體映射檔案對象,如果建立成功則通過MapViewOfFile()函數将此檔案映射對象的視圖映射進位址空間,同時得到此映射視圖的首址。可見,共享記憶體的建立主要是通過這兩個函數完成的。這兩個函數原形聲明如下:

HANDLE CreateFileMapping(HANDLE hFile,

LPSECURITY_ATTRIBUTES lpFileMappingAttributes,

DWORD flProtect,

DWORD dwMaximumSizeHigh,

DWORD dwMaximumSizeLow,

LPCTSTR lpName);

LPVOID MapViewOfFile(HANDLE hFileMappingObject,

DWORD dwDesiredAccess,

DWORD dwFileOffsetHigh,

DWORD dwFileOffsetLow,

DWORD dwNumberOfBytesToMap);

  CreateFileMapping()函數參數hFile指定了待映射到程序位址空間的檔案句柄,如果為無效句柄則系統會建立一個使用來自頁檔案而非指定磁盤檔案存儲器的檔案映射對象。很顯然,在本例中為了資料能快速交換,需要人為将此參數設定為INVALID_HANDLE_VALUE;參數flProtect設定了系統對頁面采取的保護屬性,由于需要進行讀寫操作,是以可以設定保護屬性PAGE_READWRITE;雙字型參數dwMaximumSizeHigh和dwMaximumSizeLow指定了所開辟共享記憶體區的最大位元組數;最後的參數lpName用來給此共享記憶體設定一個名字,接收程式可以通過這個名字将其打開。MapViewOfFile()函數的參數hFileMappingObject為CreateFileMapping()傳回的記憶體檔案映像對象句柄;參數dwDesiredAccess再次指定對其資料的通路方式,而且需要同CreateFileMapping()函數所設定的保護屬性相比對。這裡對保護屬性的重複設定可以確定應用程式能更多的對資料的保護屬性進行有效控制。下面給出建立共享記憶體的部分關鍵代

hRecvMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, 0, 1000000, "DataMap");

if (hRecvMap != NULL)

{

lpData = (LPBYTE)MapViewOfFile(hRecvMap, FILE_MAP_WRITE, 0, 0, 0);

if (lpData == NULL)

{

CloseHandle(hRecvMap);

hRecvMap = NULL;

}

}

// 通知接收程式記憶體檔案映射對象的視圖已經打開

HWND hRecv = ::FindWindow(NULL, DECODE_PROGRAMM);

if (hRecv != NULL)

::PostMessage(hRecv, WM_MAP_OPEN, 0, 0);

  資料的傳送實際是将資料從發送方寫到共享記憶體中,然後由接收程式及時從中取走即可。資料從發送方程式寫到共享記憶體比較簡單,隻需用memcpy()函數将資料拷貝過去,關鍵在于能及時通知接收程式資料已寫入到共享記憶體,并讓其即使取走。在這裡仍采取消息通知的方式,當資料寫入共享記憶體後通過PostMessage()函數向接收方程式發送消息,接收方在消息響應函數中完成對資料的讀取:

// 資料複制到共享記憶體

memcpy(lpData, RecvBuf, sizeof(RecvBuf));

// 通知接收方接收資料

HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);

if (hDeCode != NULL)

::PostMessage(hDeCode, WM_DATA_READY, (WPARAM)0, (LPARAM)sizeof(RecvBuf));

  當資料傳輸結束,即将退出程式時,需要将映射進來的記憶體檔案映射對象視圖解除安裝和資源的釋放等處理。這部分工作主要由UnmapViewOfFile()和CloseHandle()等函數完成:

HWND hDeCode = ::FindWindow(NULL, DECODE_PROGRAMM);

if (hDeCode != NULL)

::PostMessage(hDeCode, WM_MAP_CLOSE, 0, 0);

if (lpData != NULL)

{

UnmapViewOfFile(lpData);

lpData = NULL;

}

if (hRecvMap != NULL)

{

CloseHandle(hRecvMap);

hRecvMap = NULL;

}

  在接收程式中,在收到由發送放發出的WM_MAP_OPEN消息後,由OpenFileMapping()函數打開由名字"DataMap"指定的檔案映射對象,如果執行成功,繼續用MapViewOfFile()函數将此檔案映射對象的視圖映射到接收應用程式的位址空間并得到其首址:

m_hReceiveMap = OpenFileMapping(FILE_MAP_READ, FALSE, "DataMap");

if (m_hReceiveMap == NULL)

return;

m_lpbReceiveBuf = (LPBYTE)MapViewOfFile(m_hReceiveMap,FILE_MAP_READ,0,0,0);

if (m_lpbReceiveBuf == NULL)

{

CloseHandle(m_hReceiveMap);

m_hReceiveMap=NULL;

}

  當發送方程式将資料寫入到共享記憶體後,接收方将收到消息WM_DATA_READY,在響應函數中将資料從共享記憶體複制到本地緩存中,再進行後續的處理。同發送程式類似,在接收程式資料接收完畢後,也需要用UnmapViewOfFile()、CloseHandle()等函數完成對檔案視圖等打開過資源的釋放:

// 從共享記憶體接收資料

memcpy(RecvBuf, (char*)(m_lpbReceiveBuf), (int)lParam);

……

// 程式退出前資源的釋放

UnmapViewOfFile(m_lpbReceiveBuf);

m_lpbReceiveBuf = NULL;

CloseHandle(m_hReceiveMap);

m_hReceiveMap = NULL;

  小結

  經實際測試,使用共享記憶體在處理大資料量資料的快速交換時表現出了良好的性能,在資料可靠性等方面要遠遠高于發送WM_COPYDATA消息的方式。這種大容量、高速的資料共享處理方式在設計高速數傳通訊類軟體中有着很好的使用效果。本文所述代碼在Windows 2000下由Microsoft Visual C++ 6.0編譯通過。