昨天在上篇blog里描寫了如何把STL容器放到共享內(nèi)存里去,不過由于好久不寫blog,發(fā)覺詞匯組織能力差了很多,不少想寫的東西寫的很零散,今天剛好翻看自己的書簽,看到一篇挺老的文章,不過從共享內(nèi)存到STL容器講述得蠻全面,還提供了學(xué)習(xí)的實(shí)例,所以順便翻譯過來,并附上原文地址。
共享內(nèi)存(shm)是當(dāng)前主流UNIX系統(tǒng)中的一種IPC方法,它允許多個(gè)進(jìn)程把同一塊物理內(nèi)存段(segment)映射(map)到它們的地址空間中去。既然內(nèi)存段對(duì)于各自附著(attach)的進(jìn)程是共享的,這些進(jìn)程可以很方便的通過這塊共享內(nèi)存上的共有數(shù)據(jù)進(jìn)行通信。因此,顧名思義,共享內(nèi)存就是進(jìn)程之間共享的一組內(nèi)存段。當(dāng)一個(gè)進(jìn)程附著到一塊共享內(nèi)存上后,它得到一個(gè)指向這塊共享內(nèi)存的指針;該進(jìn)程可以像使用其他內(nèi)存一樣使用這塊共享內(nèi)存。當(dāng)然,由于這塊內(nèi)存同樣會(huì)被其他進(jìn)程訪問或?qū)懭耄员仨氁⒁膺M(jìn)程同步問題。
參考如下代碼,這是UNIX系統(tǒng)上使用共享內(nèi)存的一般方法(注:本文調(diào)用的是POSIX函數(shù)):
//Get shared memory id
//shared memory key
const key_t ipckey = 24568;
//shared memory permission; can be
//read and written by anybody
const int perm = 0666;
//shared memory segment size
size_t shmSize = 4096;
//Create shared memory if not
//already created with specified
//permission
int shmId = shmget
(ipckey,shmSize,IPC_CREAT|perm);
if (shmId ==-1) {
//Error
}
//Attach the shared memory segment
void* shmPtr = shmat(shmId,NULL,0);
struct commonData* dp = (struct commonData*)shmPtr;
//detach shared memory
shmdt(shmPtr);
|
存放在共享內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)
當(dāng)保存數(shù)據(jù)到共享內(nèi)存中時(shí)需要留意,參考如下結(jié)構(gòu):
struct commonData {
int sharedInt;
float sharedFloat;
char* name;
Struct CommonData* next;
};
|
進(jìn)程A把數(shù)據(jù)寫入共享內(nèi)存:
//Attach shared memory
struct commonData* dp =
(struct commonData*) shmat
(shmId,NULL,0);
dp->sharedInt = 5;
.
.
dp->name = new char [20];
strcpy(dp->name,"My Name");
dp->next = new struct commonData();
|
稍后,進(jìn)程B把數(shù)據(jù)讀出:
struct commonData* dp =
(struct commonData*) shmat
(shmId,NULL,0);
//count = 5;
int count = dp->sharedInt;
//problem
printf("name = [%s]\n",dp->name);
dp = dp->next; //problem
|
結(jié)構(gòu) commonData
的成員 name
和指向下一個(gè)結(jié)構(gòu)的 next
所指向的內(nèi)存分別從進(jìn)程A的地址空間中的堆上分配,顯然 name 和 next 指向的內(nèi)存也只有進(jìn)程A可以訪問。當(dāng)進(jìn)程B訪問 dp->name
或者 dp->next
時(shí)候,由于它在訪問自己地址空間以外的內(nèi)存空間,所以這將是非法操作(memory violation),它無法正確得到 name
和 next
所指向的內(nèi)存。因此,所有的共享內(nèi)存中的指針必須同樣指向共享內(nèi)存中的地址。(這也是為什么包含虛函數(shù)繼承的C++類對(duì)象不能放到共享內(nèi)存中的原因——這是另外一個(gè)話題。注:因?yàn)樘摵瘮?shù)的具體實(shí)現(xiàn)可能會(huì)在其他的內(nèi)存空間中)由于這些條件限制,放入共享內(nèi)存中的結(jié)構(gòu)應(yīng)該簡(jiǎn)單簡(jiǎn)單。(注:我覺得最好避免使用指針)
共享內(nèi)存中的STL容器
想像一下把STL容器,例如map, vector, list等等,放入共享內(nèi)存中,IPC一旦有了這些強(qiáng)大的通用數(shù)據(jù)結(jié)構(gòu)做輔助,無疑進(jìn)程間通信的能力一下子強(qiáng)大了很多。我們沒必要再為共享內(nèi)存設(shè)計(jì)其他額外的數(shù)據(jù)結(jié)構(gòu),另外,STL的高度可擴(kuò)展性將為IPC所驅(qū)使。STL容器被良好的封裝,默認(rèn)情況下有它們自己的內(nèi)存管理方案。當(dāng)一個(gè)元素被插入到一個(gè)STL列表(list)中時(shí),列表容器自動(dòng)為其分配內(nèi)存,保存數(shù)據(jù)。考慮到要將STL容器放到共享內(nèi)存中,而容器卻自己在堆上分配內(nèi)存。一個(gè)最笨拙的辦法是在堆上構(gòu)造STL容器,然后把容器復(fù)制到共享內(nèi)存,并且確保所有容器的內(nèi)部分配的內(nèi)存指向共享內(nèi)存中的相應(yīng)區(qū)域,這基本是個(gè)不可能完成的任務(wù)。例如下邊進(jìn)程A所做的事情:
//Attach to shared memory
void* rp = (void*)shmat(shmId,NULL,0);
//Construct the vector in shared
//memory using placement new
vector<int>* vpInA = new(rp) vector<int>*;
//The vector is allocating internal data
//from the heap in process A's address
//space to hold the integer value
(*vpInA)[0] = 22;
|
然后進(jìn)程B希望從共享內(nèi)存中取出數(shù)據(jù):
vector<int>* vpInB =
(vector<int>*) shmat(shmId,NULL,0);
//problem - the vector contains internal
//pointers allocated in process A's address
//space and are invalid here
int i = *(vpInB)[0];
|
重用STL allocator
進(jìn)一步考察STL容器,我們發(fā)現(xiàn)它的模板定義中有第二個(gè)默認(rèn)參數(shù),也就是allocator 類,該類實(shí)際是一個(gè)內(nèi)存分配模型。默認(rèn)的allocator是從堆上分配內(nèi)存(注:這就是STL容器的默認(rèn)表現(xiàn),我們甚至可以改造它從一個(gè)網(wǎng)絡(luò)數(shù)據(jù)庫(kù)中分配空間,保存數(shù)據(jù))。下邊是 vector 類的一部分定義:
template<class T, class A = allocator<T> >
class vector {
//other stuff
};
|
考慮如下聲明:
//User supplied allocator myAlloc
vector<int,myAlloc<int> > alocV;
|
假設(shè) myAlloc
從共享內(nèi)存上分配內(nèi)存,則 alocV
將完全在共享內(nèi)存上被構(gòu)造,所以進(jìn)程A可以如下:
//Attach to shared memory
void* rp = (void*)shmat(shmId,NULL,0);
//Construct the vector in shared memory
//using placement new
vector<int>* vpInA =
new(rp) vector<int,myAlloc<int> >*;
//The vector uses myAlloc<int> to allocate
//memory for its internal data structure
//from shared memory
(*v)[0] = 22;
|
進(jìn)程B可以如下讀出數(shù)據(jù):
vector<int>* vpInB =
(vector<int,myAlloc<int> >*) shmat
(shmId,NULL,0);
//Okay since all of the vector is
//in shared memory
int i = *(vpInB)[0];
|
所有附著在共享內(nèi)存上的進(jìn)程都可以安全的使用該vector。在這個(gè)例子中,該類的所有內(nèi)存都在共享內(nèi)存上分配,同時(shí)可以被其他的進(jìn)程訪問。只要提供一個(gè)用戶自定義的allocator,任何STL容器都可以安全的放置到共享內(nèi)存上。
一個(gè)基于共享內(nèi)存的STL Allocator
清單 shared_allocator.hh 是一個(gè)STL Allocator的實(shí)現(xiàn),SharedAllocator
是一個(gè)模板類。而 Pool
類完成共享內(nèi)存的分配與回收。
template<class T>class SharedAllocator {
private:
Pool pool_; // pool of elements of sizeof(T)
public:
typedef T value_type;
typedef unsigned int size_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
pointer address(reference r) const { return &r; }
const_pointer address(const_reference r) const {return &r;}
SharedAllocator() throw():pool_(sizeof(T)) {}
template<class U> SharedAllocator
(const SharedAllocator<U>& t) throw():
pool_(sizeof(T)) {}
~SharedAllocator() throw() {};
// space for n Ts
pointer allocate(size_t n, const void* hint=0)
{
return(static_cast<pointer> (pool_.alloc(n)));
}
// deallocate n Ts, don't destroy
void deallocate(pointer p,size_type n)
{
pool_.free((void*)p,n);
return;
}
// initialize *p by val
void construct(pointer p, const T& val) { new(p) T(val); }
// destroy *p but don't deallocate
void destroy(pointer p) { p->~T(); }
size_type max_size() const throw()
{
pool_.maxSize();
}
template<class U>
// in effect: typedef SharedAllocator<U> other
struct rebind { typedef SharedAllocator<U> other; };
};
template<class T>bool operator==(const SharedAllocator<T>& a,
const SharedAllocator<T>& b) throw()
{
return(a.pool_ == b.pool_);
}
template<class T>bool operator!=(const SharedAllocator<T>& a,
const SharedAllocator<T>& b) throw()
{
return(!(a.pool_ == b.pool_));
}
|
清單pool.hh是 Pool
類定義,其中靜態(tài)成員shm_
是類型 shmPool
,保證每個(gè)進(jìn)程只有唯一的一個(gè)shmPool
實(shí)例。shmPool
ctor 創(chuàng)建并附著所需大小的內(nèi)存到共享內(nèi)存上。共享內(nèi)存的參數(shù),比如 鍵值、段數(shù)目、段大小,都通過環(huán)境變量傳遞給 shmPool
ctor。成員 segs_
是共享段的數(shù)目,segSize_
是每個(gè)共享段的大小,成員path_
和key_
用來創(chuàng)建唯一的 ipckey
。shmPool
為每個(gè)共享段創(chuàng)建一個(gè)信號(hào)量(semaphore)用于同步。shmPool
還在為每個(gè)共享段構(gòu)造了一個(gè) Chunk
類,一個(gè) Chunk
代表一個(gè)共享段。每個(gè)共享段的標(biāo)識(shí)是shmId_
, 信號(hào)量 semId_
控制該段的訪問許可,一個(gè)指向 Link
結(jié)構(gòu)的指針表明 Chunk
類的剩余列表。
class Pool {
private:
class shmPool {
private:
struct Container {
containerMap* cont;
};
class Chunk {
public:
Chunk()
Chunk(Chunk&);
~Chunk() {}
void* alloc(size_t size);
void free (void* p,size_t size);
private:
int shmId_;
int semId_;
int lock_()
};
int key_;
char* path_;
Chunk** chunks_;
size_t segs_;
size_t segSize_;
Container* contPtr_;
int contSemId_;
public:
shmPool();
~shmPool();
size_t maxSize();
void* alloc(size_t size);
void free(void* p, size_t size);
int shmPool::lockContainer()
int unLockContainer()
containerMap* getContainer()
void shmPool::setContainer(containerMap* container)
};
private:
static shmPool shm_;
size_t elemSize_;
public:
Pool(size_t elemSize);
~Pool() {}
size_t maxSize();
void* alloc(size_t size);
void free(void* p, size_t size);
int lockContainer();
int unLockContainer();
containerMap* getContainer();
void setContainer(containerMap* container);
};
inline bool operator==(const Pool& a,const Pool& b)
{
return(a.compare(b));
}
|
把STL容器放入共享內(nèi)存
假設(shè)進(jìn)程A在共享內(nèi)存中放入了數(shù)個(gè)容器,進(jìn)程B如何找到這些容器呢?一個(gè)方法就是進(jìn)程A把容器放在共享內(nèi)存中的確定地址上(fixed offsets),則進(jìn)程B可以從該已知地址上獲取容器。另外一個(gè)改進(jìn)點(diǎn)的辦法是,進(jìn)程A先在共享內(nèi)存某塊確定地址上放置一個(gè)map容器,然后進(jìn)程A再創(chuàng)建其他容器,然后給其取個(gè)名字和地址一并保存到這個(gè)map容器里。進(jìn)程B知道如何獲取該保存了地址映射的map容器,然后同樣再根據(jù)名字取得其他容器的地址。清單container_factory.hh是一個(gè)容器工廠類。類Pool
的方法setContainer
把map容器放置在一個(gè)已知地址上,方法getContainer
可以重新獲取這個(gè)map。該工廠的方法用來在共享內(nèi)存中創(chuàng)建、獲取和刪除容器。當(dāng)然,傳遞給容器工廠的容器需要以SharedAllocator
作為allocator。
struct keyComp {
bool operator()(const char* key1,const char* key2)
{
return(strcmp(key1,key2) < 0);
}
};
class containerMap: public map<char*,void*,keyComp,SharedAllocator<char* > > {};
class containerFactory {
public:
containerFactory():pool_(sizeof(containerMap)){}
~containerFactory() {}
template<class Container> Container* createContainer
(char* key,Container* c=NULL);
template<class Container> Container* getContainer
(char* key,Container* c=NULL);
template<class Container> int removeContainer
(char* key,Container* c=NULL);
private:
Pool pool_;
int lock_();
int unlock_();
};
|
結(jié)論
本文描述的方案可以在共享內(nèi)存中創(chuàng)建STL容器,其中的一個(gè)缺陷是,在分配共享內(nèi)存之前,應(yīng)該保證共享內(nèi)存的總大小(segs_* segSize_
)大于你要保存STL容器的最大長(zhǎng)度,因?yàn)橐坏╊?code>Pool 超出了共享內(nèi)存的,該類無法再分配新的共享內(nèi)存。
完整的源代碼可以從這里下載:www.cuj.com/code
參考文獻(xiàn)
- Bjarne Stroustrup. The C++ Programming Language, Third Edition (Addison-Wesley, 1997).
- Matthew H. Austern. Generic Programming and the STL: Using and
Extending the C++ Standard Template Library (Addison-Wesley, 1999).
關(guān)于作者
Grum Ketema has Masters degrees in Electrical Engineering and Computer Science. With 17 years of experience in software development, he has been using C since 1985, C++ since 1988, and Java since 1997. He has worked at AT&T Bell Labs, TASC, Massachusetts Institute of Technology, SWIFT, BEA Systems, and Northrop.