充分利用共享內存並不總是容易的。在本文中,IBM 的 Sachin Agrawal 與我們共享了他的 C++ 專門技術,展示了 面向對象 如何去利用一個獨特而實用的進程間通信通道的關鍵優勢。 就時間和空間而言,共享內存可能是所有現代操作系統都具備的最高效的進程間通信
充分利用共享內存並不總是容易的。在本文中,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 <s
tdio.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;
case 2:
pA->WhoAmI();
case 3:
cout << pB->m_nA << endl;
case 4:
pB->WhoAmI();
case 5:
cout << pC->m_nA << endl;
case 6:
pC->WhoAmI();
}
return 0;
}
#include <pthread.h>
void DoNothingCode() {
pthread_create(NULL, NULL, NULL, NULL);
}
第 6-35 行
客戶機進程獲得指向三個共享對象的指針,建立對它們的數據成員的三個引用,並且 —— 依賴於命令行的輸入 —— 調用三個虛函數。
第 36-39 行
沒有被調用的 pthread_create() 函數用來強制鏈接到另一個共享庫。來自所有共享庫的任何方法都可以滿足這一目的。
共享庫和客戶機可執行文件的兩個實例的編譯方法如下:
g
clearcase/" target="_blank" >cc shared g shm_server.cpp o libshm_server.so lstdc++
gcc -g shm_client.cpp -o shm_client1 -lpthread -lshm_server -L .
gcc -g shm_client.cpp -o shm_client2 -lshm_server -L . lpthread
注意,交換了 shm_client1 和 shm_client2 中 shm_server 和 pthread 的鏈接順序,以確保 shm_server 共享庫在兩個可執行文件中的基址不同。可以使用 ldd 命令進一步驗證這一點。示例輸出通常如下所示:
清單 4. shm_client1 的庫映射
ldd shm_client1
libpthread.so.0 => (0x4002d000)
libshm_server.so => (0x40042000)
libc.so.6 => (0x4005b000)
ld-
linux.so.2 => (0x40000000)
清單 5. shm_client2 的庫映射
<ccid_nobr>
<table width="400" border="1" cellspacing="0" cellpadding="2"
bordercolorlight = "black" bordercolordark = "#FFFFFF" align="center">
<tr>
<td bgcolor="e6e6e6" class="code" >
<pre><ccid_code> ldd shm_client2
libshm_server.so => (0x40018000)
libpthread.so.0 => (0x40046000)
libc.so.6 => (0x4005b000)
ld-linux.so.2 => (0x40000000)
這裡的主要目的是使構建的兩個客戶機二進制文件具有不同的
服務器庫基址。在這個示例程序的上下文中,使用不被調用的 pthread_create() 函數和不同的共享庫鏈接順序來達到這一目標。不過,沒有具體規則或統一步驟可以作用於所有鏈接;需要根據不同的情況采取不同的方法。
例 1:shm_client1 與 shm_client1
在下面的輸出中,首先在 shell 中調用 shm_client1。由於現在沒有共享對象,於是 shm_client1 創建了它們,引用它們的數據成員,調用它們的虛函數,然後退出 —— 將對象留在了內存中。第二次,進程只是引用數據成員和虛函數。
清單 6. shm_client1 與 shm_client1 的輸出日志
$ ./shm_client1
Created the shared memory
1073844224 1073845248 1073846272
1
Object type: A
2
Object type: B
3
Object type: C
$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x000004d2 2260997 sachin 666 3072 0
$ ./shm_client1
1073840128 1073841152 1073842176
1
Object type: A
2
Object type: B
-> 0
-> Segmentation fault (core dumped)
當第二個進程試圖通過類型 C * 的指針去引用數據成員 A::m_nA 時(您會記得 C 虛擬繼承自 A),共享對象內的基子對象(base sub-object)指針會被讀取。共享對象是在現在不存在的進程的上下文中構造的。因此,讀取 A::m_nA 和 C::WhoAmI() 時讀入的是內存垃圾。
因為 Vee-Table 和虛函數位於 shm_server 共享庫內部,恰巧在同一虛擬地址被重新加載,所以,再次引用類型 A * 和 B * 的指針時不會觀察到任何問題。
因此,GNU 所采用的 C++ 對象模型沒有成功地處理虛擬繼承。
例 2:shm_client1 與 shm_client2
在下一個示例輸出中,在命令行中首先執行 shm_client1,然後執行 shm_client2:
清單 7. shm_client1 與 shm_client2 的輸出日志
$ ./shm_client1
Created the shared memory
1073844224 1073845248 1073846272
1
Object type: A
2
Object type: B
3
Object type: C
$ ipcs
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x000004d2 2359301 sachin 666 3072 0
$ ./shm_client2
1073942528 1073943552 1073944576
1
-> Segmentation fault (core dumped)
$ ./shm_client2 3
1073942528 1073943552 1073944576
2
-> Segmentation fault (core dumped)
$ ./shm_client2 5
1073942528 1073943552 1073944576
-> 1048594
-> Segmentation fault (core dumped)
然而,Vee-Table 位於 shm_server 共享庫內部:在 shm_client1 和 shm_client2 中它被加載到不同的虛地址。這樣,讀取 A::WhoAmI() 和 B::WhoAmI() 時讀入的都是內存垃圾。
運用共享內存 在共享內存中具體使用 C++ 對象時您應該考慮兩個主要問題。首先,Vee-Table 指針用於訪問虛函數,而對數據成員的訪問直接使用編譯時偏移量實現。因此,對於所有這種共享對象來說,所有進程中的 Vee-Table 和虛函數都應該具有相同的虛地址。關於這一點,沒有一成不變的規則,不過,為相關的共享庫采用適當的鏈接順序大部分時候都會管用。
另外,永遠不要忘記,虛擬繼承的對象有指向基對象的基指針。基指針引用進程的數據段,而且永遠是特定於進程的。難以確保所有的客戶機進程中都有相同的數字值。因此,假如使用 C++ 對象模型,要避免在共享內存中構造虛擬繼承的對象。但是,也不要忘記,不同的編譯器采用的是不同的對象模型。例如,Microsoft Compiler 使用進程無關的偏移量來為虛擬繼承類指定基對象,因而不存在這個問題。重要的是,確保所有客戶機進程中的共享對象的地址相同。