引言
進程間的資料交換和共用是一種非常重要和實用的技術。大、中型軟體的開發設計多是由眾多程式設計人員的合作完成,通常一個程式設計人員只負責其中一個或幾個模組的開發,這些模組可以是動態連結程式庫也可以是應用程式或是其他形式的程式組件。這些獨立開發出來的程式模組最終需要作為一個整體來運行,即組成一個系統,在系統運行期間這些模組往往需要頻繁地進行資料交換和資料共用,對於動態連結程式庫同其主調應用程式之間的資料交換是非常容易實現的,但是在兩個應用程式之間或是動態連結程式庫同其主調應用程式之外的其他應用程式進行資料交換就比較困難了。尤其是在交換資料量過大、交換過於頻繁的情況下更是難以實現,本文即對此展開討論,並提出了一種通過共用記憶體來實現進程見大資料量快速交換的一種方法。
通訊方式的比較和選擇
進程間通訊的方式有很多,常用的有共用記憶體、具名管道和匿名管道、發送訊息等幾種方法來直接完成,另外還可以通過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編譯通過。