充分利用共享內存並不總是容易的。在本文中,IBM 的 Sachin Agrawal 與我們共享了他的 C++ 專門技術,展示了面向對象如何去利用一個獨特而實用的進程間通信通道的關鍵優勢。 就時間和空間而言,共享內存可能是所有現代操作系統都具備的最高效的進程間通信通道。共享內存 同時將地址空間映射到多個進程:一個進程只需依附到共享內存並像使用普通內存一樣使用它,就可以開始與其他進程進行通信。 不過,在面向對象編程領域中,進程更傾向於使用共享對象而不是原始的信息。通過對象,不需要再對對象中容納的信息進行序列化、傳輸和反序列化。共享對象也駐留在共享內存中,盡管這種對象“屬於”創建它們的進程,但是系統中的所有進程都可以訪問它們。因此,共享對象中的所有信息都應該是嚴格與特定進程無關的。 這與當前所有流行的編譯器所采用的 C++ 模型是直接矛盾的:C++ 對象中總是包含指向各種 Vee-Table 和子對象的指針,這些是 與特定進程相關的。要讓這種對象可以共享,您需要確保在所有進程中這些指針的目標都駐留在相同的地址。 在一個小的示例的幫助下,本文展示了在哪些情況下 C++ 模型可以成功使用共享內存模型,哪些情況下不能,以及可能從哪裡著手。討論和示例程序都只限於非靜態數據成員和虛函數。其他情形不像它們這樣與 C++ 對象模型關系密切:靜態的和非靜態非虛擬的成員函數在共享環境中沒有任何問題。每個進程的靜態成員不駐留在共享內存中(因此沒有問題),而共享的靜態成員的問題與這裡討論到的問題類似。
環境假定 本文僅局限於用於 32 位 x86 Interl 體系結構的 Red Hat Linux 7.1,使用版本 2.95 的 GNU C++ 編譯器及相關工具來編譯和測試程序。不過,您同樣可以將所有的思想應用到任意的機器體系結構、操作系統和編譯器組合。
示例程序 示例程序由兩個客戶機構成:shm_client1 和 shm_client2,使用由共享對象庫 shm_server 提供的共享對象服務。對象定義在 common.h 中: 清單 1. common.h 中的定義 #ifndef __COMMON_H__ #define __COMMON_H__ class A { public: int m_nA; virtual void WhoAmI(); static void * m_sArena; void * operator new (unsigned int); }; class B : public A { public: int m_nB; virtual void WhoAmI(); }; class C : virtual public A { public: int m_nC; virtual void WhoAmI(); }; void GetObjects(A ** pA, B ** pB, C ** pC); #endif //__COMMON_H__ 清單 1 定義了三個類(A、B 和 C),它們有一個共同的虛函數 WhoAmI()。基類 A 有一個名為 m_nA 的成員。定義靜態成員 m_sArena 和重載操作 new() 是為了可以在共享內存中構造對象。類 B 簡單地從 A 繼承,類 C 從 A 虛擬地繼承。為了確保 A、B 和 C 的大小明顯不同,定義了 B::m_nB 和 C::m_nC。這樣就簡化了 A::operator new() 的實現。GetObjects() 接口返回共享對象的指針。 共享庫的實現在 shm_server.cpp 中: 清單 2. 庫 - shm_server.cpp #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <errno.h> #include <stdio.h> #include <iostream> #include "common.h" void * A::m_sArena = NULL; void * A::operator new (unsigned int size) { switch (size) { case sizeof(A): return m_sArena; case sizeof(B): return (void *)((int)m_sArena + 1024); case sizeof(C): return (void *)((int)m_sArena + 2048); default: cerr << __FILE__ << ":" << __LINE__ << " Critical error" << endl; } } void A::WhoAmI() { cout << "Object type: A" << endl; } void B::WhoAmI() { cout << "Object type: B" << endl; } void C::WhoAmI() { cout << "Object type: C" << endl; } void GetObjects(A ** pA, B ** pB, C ** pC) { *pA = (A *)A::m_sArena; *pB = (B *)((int)A::m_sArena + 1024); *pC = (C *)((int)A::m_sArena + 2048); } class Initializer { public: int m_shmid; Initializer(); static Initializer m_sInitializer; }; Initializer Initializer::m_sInitializer; Initializer::Initializer() { key_t key = 1234; bool bCreated = false; m_shmid = shmget(key, 3*1024, 0666); if (-1 == m_shmid) { if (ENOENT != errno) { cerr << __FILE__ << ":" << __LINE__ << " Critical error" << endl; return; } m_shmid = shmget(key, 3*1024, IPC_CREAT 0666); if (-1 == m_shmid) { cerr << __FILE__ << ":" << __LINE__ << " Critical error" << endl; return; } cout << "Created the shared memory" << endl; bCreated = true; } A::m_sArena = shmat(m_shmid, NULL, 0); if (-1 == (int)A::m_sArena) { cerr << __FILE__ << ":" << __LINE__ << " Critical error" << endl; return; } if (bCreated) { // ConstrUCt objects on the shared memory A * pA; pA = new A; pA->m_nA = 1; pA = new B; pA->m_nA = 2; pA = new C; pA->m_nA = 3; } return; } 讓我們更詳細地研究清單 2: 第 9-25 行:operator new () 同一個重載的操作符讓您可以在共享內存中構造類 A、B 和 C 的對象。對象 A 直接從共享內存的起始處開始。對象 B 從偏移量 1024 處開始,C 從偏移量 2048 處開始。 第 26-34 行:虛函數 虛函數簡單地向標准輸出寫一行文本。 第 35-39 行:GetObjects GetObjects() 返回指向共享對象的指針。 第 40-46 行:初始化器(Initializer) 這個類存儲共享內存標識符。它的構造函數創建共享內存及其中的對象。如果共享內存已經存在,它就只是依附到現有的共享內存。靜態成員 m_sInitializer 確保在使用共享庫的客戶機模塊的 main() 函數之前調用構造函數。 第 48-82 行:Initializer::Initializer() 如果原來沒有共享內存,則創建,並在其中創建共享對象。如果共享內存已經存在,對象的構造就會被跳過。Initializer::m_shmid 記錄標識符,A::m_sArena 記錄共享內存地址。 即使所有進程都不再使用它了,共享內存也不會被銷毀。這樣就讓您可以顯式地使用 ipcs 命令來銷毀它,或者使用 ipcs 命令進行一些速查。 客戶機進程的實現在 shm_client.cpp 中: 清單 3. 客戶機 - shm_client.cpp #include "common.h" #include <iostream> #include <stdlib.h> int main (int argc, char * argv[]) { int jumpTo = 0; if (1 < argc) { jumpTo = strtol(argv[1], NULL, 10); } if ((1 > jumpTo) (6 < jumpTo)) { jumpTo = 1; } A * pA; B * pB; C * pC; GetObjects(&pA, &pB, &pC); cout << (int)pA << "\t"; cout << (int)pB << "\t"; cout << (int)pC << "\n"; switch (jumpTo) { case 1: cout << pA->m_nA << endl; ca