本文中將學習如何使用 Boost IPC 庫實現共享內存對象、消息隊列和同步文件鎖。通過使用 Boost MPI 庫,瞭解 environment 和 communicator 類,以及如何實現分佈式通信。
注意:本文中的代碼已經用 gcc-4.3.4 和 boost-1.45 包測試過了。
使用 Boost IPC 庫
Boost Interprocess 是一個只由頭文件組成的庫,所以您需要做的只是在自己的源代碼中包含適當的頭文件並讓編譯器知道 include 路徑。這是一個非常好的特性;您只需下載 Boost 源代碼(見 參考資料 中的鏈接),然後就可以開始使用了。例如,要想在自己的代碼中使用共享內存,就使用 清單 1 所示的 include。
清單 1. Boost IPC 庫只由頭文件組成
#include <boost/interprocess/shared_memory_object.hpp> using namespace boost::interprocess; //… your sources follow …
在把信息傳遞給編譯器時,您要求進程根據安裝相應地修改 include 路徑。然後,編譯代碼:
bash-4.1$ g++ ipc1.cpp –I../boost_1_45_0
創建共享內存對象
我們先從傳統的 "Hello World!" 程序開始。有兩個進程:第一個進程把字符串 "Hello World!" 寫入內存,另一個進程讀取並顯示此字符串。像 清單 2 這樣創建共享內存對象。
清單 2. 創建共享內存對象
#include <boost/interprocess/shared_memory_object.hpp> int main(int argc, char* argv[ ]) { using namespace using boost::interprocess; try { // creating our first shared memory object. shared_memory_object sharedmem1 (create_only, "Hello", read_write); // setting the size of the shared memory sharedmem1.truncate (256); // … more code follows } catch (interprocess_exception& e) { // .. . clean up } }
sharedmem1 對象的類型是 shared_memory_object(在 Boost 頭文件中聲明並定義),它的構造函數有三個參數:
-
第一個參數 —
create_only
— 表示要創建這個共享內存對象而且還沒有創建它。如果已經存在同名的共享對象,就會拋出異常。對於希望訪問已經創建的共享內存的進程,第一個參數應該是open_only
。 -
第二個參數 —
Hello
— 是共享內存區域的名稱。另一個進程將使用這個名稱訪問這個共享內存。 -
第三個參數 —
read_write
— 是共享內存對象的訪問指示符。因爲這個進程要修改共享內存對象的內容,所以使用read_write
。只從共享內存讀取數據的進程使用read_only
指示符。
使用共享內存對象寫數據
使用共享內存對象的進程必須在自己的地址空間中映射對象。使用在頭文件 mapped_region.hpp 中聲明並定義的 mapped_region 類執行映射。使用 mapped_region 的另一個好處是可以對共享內存對象進行完全和部分訪問。清單 3 演示如何使用 mapped_region。
清單 3. 使用 mapped_region 訪問共享內存對象
#include <boost/interprocess/shared_memory_object.hpp> #include <boost/interprocess/mapped_region.hpp> int main(int argc, char* argv[ ]) { using namespace boost::interprocess; try { // creating our first shared memory object. shared_memory_object sharedmem1 (create_only, "Hello", read_write); // setting the size of the shared memory sharedmem1.truncate (256); // map the shared memory to current process mapped_region mmap (sharedmem1, 256); // access the mapped region using get_address std::strcpy(static_cast<char* >(region.get_address()), "Hello World!\n"); } catch (interprocess_exception& e) { // .. . clean up } }
就這麼簡單。現在已經創建了您自己的 mapped_region 對象並使用 get_address 方法訪問了它。執行了 static_cast,因爲 get_address 返回一個 void*。
當主進程退出時共享內存會怎麼樣?
當主進程退出時,並不刪除共享內存。要想刪除共享內存,需要調用 shared_memory_object::remove。第二個進程的訪問機制也很簡單:清單 4 證明了這一點。
清單 4. 從第二個進程訪問共享內存對象
#include <boost/interprocess/shared_memory_object.hpp> #include <boost/interprocess/mapped_region.hpp> #include <cstring> #include <cstdlib> #include <iostream> int main(int argc, char *argv[ ]) { using namespace boost::interprocess; try { // opening an existing shared memory object shared_memory_object sharedmem2 (open_only, "Hello", read_only); // map shared memory object in current address space mapped_region mmap (sharedmem2, read_only); // need to type-cast since get_address returns void* char *str1 = static_cast<char*> (mmap.get_address()); std::cout << str1 << std::endl; } catch (interprocess_exception& e) { std::cout << e.what( ) << std::endl; } return 0; }
在清單 4 中,使用 open_only 和 read_only 屬性創建共享內存對象。如果無法找到這個共享內存對象,就會拋出異常。現在,構建並運行 清單 3 和 清單 4 中的代碼。應該會在終端上看到 "Hello World!"。
接下來,在第二個進程的代碼(清單 4)中 std::cout 後面添加以下代碼並重新構建代碼:
// std::cout code here shared_memory_object::remove("Hello"); // } catch(interprocess_exception& e) {
連續執行代碼兩次,第二次執行會顯示 "No such file or directory",這證明共享內存已經被刪除了。
使用消息隊列實現進程間通信
現在,研究另一種流行的進程間通信機制:消息隊列。每個參與通信的進程都可以在隊列中添加消息和從隊列讀取消息。消息隊列具有以下性質:
- 它有名稱,進程使用名稱訪問它。
- 在創建隊列時,用戶必須指定隊列的最大長度和一個消息的最大大小。
-
隊列是持久的,這意味着當創建它的進程死亡之後它仍然留在內存中。可以通過顯式地調用
boost::interprocess::message_queue::remove
刪除隊列。
在 清單 5 所示的代碼片段中,進程創建了一個可包含 20 個整數的消息隊列。
清單 5. 創建一個可包含 20 個整數的消息隊列
#include <boost/interprocess/ipc/message_queue.hpp> #include <iostream> int main(int argc, char* argv[ ]) { using namespace boost::interprocess; try { // creating a message queue message_queue mq (create_only, // only create "mq", // name 20, //max message count sizeof(int) //max message size ); // … more code follows } catch (interprocess_exception& e) { std::cout << e.what( ) << std::endl; } }
注意傳遞給 message_queue 的構造函數的 create_only 屬性。與共享內存對象相似,對於以只讀方式打開消息隊列,應該把 open_only 屬性傳遞給構造函數。
發送和接收數據
在發送方,使用隊列的 send 方法添加數據。send 方法有三個輸入參數:原始數據的指針 (void*)、數據的大小和優先級。目前,以相同的優先級發送所有數據。清單 6 給出代碼。
清單 6. 向隊列發送消息
#include <boost/interprocess/ipc/message_queue.hpp> #include <iostream> int main(int argc, char* argv[ ]) { using namespace boost::interprocess; try { // creating a message queue message_queue mq (create_only, // only create "mq", // name 20, //max message count sizeof(int) //max message size ); // now send the messages to the queue for (int i=0; i<20; ++i) mq.send(&i, sizeof(int), 0); // the 3rd argument is the priority } catch (interprocess_exception& e) { std::cout << e.what( ) << std::endl; } }
在接收方,使用 open_only 屬性創建隊列。通過調用 message_queue 類的 receive 方法從隊列獲取消息。清單 7 給出 receive 的方法簽名。
清單 7. message_queue::receive 的方法簽名
void receive (void *buffer, std::size_t buffer_size, std::size_t &recvd_size, unsigned int &priority );
我們來仔細看一下。第一個參數是從隊列接收的數據將被存儲到的位置。第二個參數是接收的數據的預期大小。第三個參數是接收的數據的實際大小。第四個參數是接收的消息的優先級。顯然,如果在執行程序期間第二個和第三個參數不相等,就是出現錯誤了。清單 8 給出接收者進程的代碼。
清單 8. 從消息隊列接收消息
#include <boost/interprocess/ipc/message_queue.hpp> #include <iostream> int main(int argc, char* argv[ ]) { using namespace boost::interprocess; try { // opening the message queue whose name is mq message_queue mq (open_only, // only open "mq" // name ); size_t recvd_size; unsigned int priority; // now send the messages to the queue for (int i=0; i<20; ++i) { int buffer; mq.receive ((void*) &buffer, sizeof(int), recvd_size, priority); if (recvd_size != sizeof(int)) ; // do the error handling std::cout << buffer << " " << recvd_size << " " << priority; } } catch (interprocess_exception& e) { std::cout << e.what( ) << std::endl; } }
這相當簡單。注意,仍然沒有從內存中刪除消息隊列;與共享內存對象一樣,這個隊列是持久的。要想刪除隊列,應該在使用完隊列之後添加以下行:
message_queue::remove("mq"); // remove the queue using its name
消息優先級
在發送方,做 清單 9 所示的修改。接收方代碼不需要修改。
清單 9. 修改消息的優先級
message_queue::remove("mq"); // remove the old queue message_queue mq (…); // create as before for (int i=0; i<20; ++i) mq.send(&i, sizeof(int), i%2); // 第 3 個參數爲消息的優先級 // … rest as usual
再次運行代碼時,應該會看到 清單 10 所示的輸出。
清單 10. 在接收進程中看到的輸出
1 4 1 3 4 1 5 4 1 7 4 1 9 4 1 11 4 1 13 4 1 15 4 1 17 4 1 19 4 1 0 4 0 2 4 0 4 4 0 6 4 0 8 4 0 10 4 0 12 4 0 14 4 0 16 4 0 18 4 0
清單 10 證實,第二個進程優先接收優先級高的消息。
同步對文件的訪問
共享內存和消息隊列很不錯,但是文件 I/O 也是重要的進程間通信工具。對併發進程用於通信的文件訪問進行同步並非易事,但是 Boost IPC 庫提供的文件鎖功能讓同步變得簡單了。在進一步解釋之前,來看一下 清單 11,瞭解 file_lock 對象是如何工作的。
清單 11. 使用 file_lock 對象同步文件訪問
#include <fstream> #include <iostream> #include <boost/interprocess/sync/file_lock.hpp> #include <cstdlib> int main() { using namespace boost::interprocess; std::string fileName("test"); std::fstream file; file.open(fileName.c_str(), std::ios::out | std::ios::binary | std::ios::trunc); if (!file.is_open() || file.bad()) { std::cout << "Open failed" << std::endl; exit(-1); } try { file_lock f_lock(fileName.c_str()); f_lock.lock(); std::cout << "Locked in Process 1" << std::endl; file.write("Process 1", 9); file.flush(); f_lock.unlock(); std::cout << "Unlocked from Process 1" << std::endl; } catch (interprocess_exception& e) { std::cout << e.what( ) << std::endl; } file.close(); return 0; }
代碼首先打開一個文件,然後使用 file_lock 鎖定它。寫操作完成之後,它刷新文件緩衝區並解除文件鎖。使用 lock 方法獲得對文件的獨佔訪問。如果另一個進程也試圖對此文件進行寫操作並已經請求了鎖,那麼它會等待,直到第一個進程使用 unlock 自願地放棄鎖。file_lock類的構造函數接受要鎖定的文件的名稱,一定要在調用 lock 之前打開文件;否則會拋出異常。
現在,複製 清單 11 中的代碼並做一些修改。具體地說,讓第二個進程請求這個鎖。清單 12 給出相關修改。
清單 12. 試圖訪問文件的第二個進程的代碼
// .. as in Listing 11 file_lock f_lock(fileName.c_str()); f_lock.lock(); std::cout << "Locked in Process 2" << std::endl; system("sleep 4"); file.write("Process 2", 9); file.flush(); f_lock.unlock(); std::cout << "Unlocked from Process 2" << std::endl; // file.close();
現在,如果這兩個進程同時運行,有 50% 的機會看到第一個進程等待 4 秒後才獲得 file_lock,其他情況都不變。
在使用 file_lock 時,必須記住幾點。這裏討論的主題是進程間通信,重點在進程 上。這意味着,不是使用 file_lock 來同步同一進程中各個線程的數據訪問。在與 POSIX 兼容的系統上,文件句柄是進程屬性,而不是 線程屬性。下面是使用文件鎖的幾條規則:
-
對於每個進程,每個文件使用一個
file_lock
對象。 - 使用相同的線程來鎖定和解鎖文件。
-
在解鎖文件之前,通過調用
C
的flush
庫例程或flush
方法(如果喜歡使用C++ fstream
的話),刷新寫入者進程中的數據。
結合使用 file_lock 和有範圍(scope)的鎖
在執行程序時,可能會出現拋出異常而文件沒有解鎖的情況。這種情況可能會導致意外的程序行爲。爲了避免這種情況,可以考慮把file_lock 對象放在(boost/interprocess/sync/scoped_lock.hpp 中定義的)scoped_lock 中。如果使用 scoped_lock,就不需要顯式地鎖定或解鎖文件;鎖定發生在構造器內,每當您退出該範圍,就會自動發生解鎖。清單 13 給出對 清單 11 的修改,使之使用有範圍的鎖。
清單 13. 結合使用 scoped_lock 和 file_lock
#include <boost/interprocess/sync/scoped_lock.hpp> #include <boost/interprocess/sync/file_lock.hpp> //… code as in Listing 11 file_lock f_lock(fileName.c_str()); scoped_lock<file_lock> s_lock(f_lock); // internally calls f_lock.lock( ); // No need to call explicit lock anymore std::cout << "Locked in Process 1" << std::endl; file.write("Process 1", 9); // … code as in Listing 11