共享内存实用研究(一)
由于最近一段时间较忙,事情较多,一直没有时间发布新帖。因为以前做过一些游戏服务器的需求,所以希望在这里把自己的一些游戏服务器设计经验一点点共享出来,供大家参考,当然,对于我个人而言,我想借助这些文章,沉淀一下自己的知识,抽象一下以前的代码。当然,可能有的并不完善,欢迎大家提出自己的意见和想法。
由于有些代码是商业的,不能公开,所以我以我的理解重写了,或许有些问题的地方,还请各位指正。
以下代码我都会以附件的形式贴出来。这里只写一些主要的部分。
我觉得共享内存,在服务器应用中是很有用的一个东西,尤其在跨进程访问的时候,比如,我从数据库读取一些用户信息,预先放进共享内存,然后,另外的一个进程通过这个结构去访问这些数据。这样做的好处是,就算其中一个服务器宕机,只要另外的服务器还在运行,共享内存就会得以保存,当宕机的服务器重启之后,能还原到宕机那一时刻的数据。在网络游戏中应用尤其多,但是同样,共享内存是一个双刃剑,难处理的是跨进程的读写操作,容易造成一些领崩溃的问题。不过,这些困难,是可以通过设计减少它的发生概率的。
呵呵,废话少说了,先从最基本的开始吧。
由于windows和linux下的共享内存不太一样,所以写一个跨平台的共享内存模块是有一定意义的。于是,在这里,先写一个这样的东东吧。
windows的共享内存是指,
其实网上写的很多,不过还是自己写了一个,毕竟觉得自己写也不难,其实也没多少代码,关键在思路。
namespace ShareMemoryAPI
{
//创建一个ShareMemory对象指针
SMHandle CreateShareMemory(SMKey Key, int nSize);
//打开一个已经存在的ShareMemory对象指针
SMHandle OpenShareMemory(SMKey pKey, int nSize);
//得到指定的ShareMemory对象指针映射
char* MapShareMemory(SMHandle handle);
//关闭指定的ShareMemory对象指针映射
void UnMapShareMemory(char* pData);
void CloseSharememory(SMHandle handle);
};
首先封装一下简单的调用API,因为windows下用的句柄,而 linux下用的是一个序号。要想完全兼容有些麻烦,所以我设计了SMHandle对象和SMKey对象,用于兼容在windows和linux下的统一性。
其实看上面的API,就四个,不多,其实这么多也就够用了。在复杂的共享内存应用,大多也都是它们衍生的。
//如果是windows,定义对象
#ifdef _WIN32
#define INIT_HANDLE 0xFFFFFFFFFFFFFFFF
typedef HANDLE SMHandle;
typedef int SMKey;
#endif
//如果是linux,定义对象
#ifdef _LINUX
typedef int SMHandle;
typedef key_t SMKey;
#endif
这里,我约定了所有的ShareMemory的标记都是int,为了统一,当在windows的时候,我会把这个标记转化为char*。
然后,我建立了一个类,负责记录用户创建的ShareMemory对象。
class CSMAccessObject
{
public:
CSMAccessObject(void);
~CSMAccessObject(void);
//创建对象
bool Create(SMKey key, int nSize);
//删除对象
void Destory();
//检测对象是否存在,如果存在则不再创建,直接打开
bool Open(SMKey key, int nSize);
//得到指定长度偏移的指针位置
char* GetData(int nSize);
//得到此对象的总长度
int GetSize();
private:
SMHandle m_SMHandle;
SMKey m_Key;
int m_nSize;
char* m_pData;
};
这个对象的主要目的是,创建或者维护一个块指定长度的CSMAccessObject对象。 这里的方法都是用ShareMemoryAPI下的命令处理的。
这里要说明的是在windows下,SMHandle的类型是Handle类型,一个句柄指针。而在linux下,是int类型,一个位置。当然,在64位的系统下,这个int要注意它的实际长度。而key关键字就比较麻烦了,在wondiws下,key可以是一个char*,一个字符串,而在linux下,这个key是一个key_t类型(实际是一个int类型),而这个key必须通过匹配获得。这个匹配是指定一个linux的共享内存路径,比如ftok("/home/freeeyes/src/SMData/", 0);这将会返回一个key类型。如果返回的是-1表示获取唯一ID失败,这和windows返回句柄为NULL(0)是不同的。
windows的共享内存机制是,当有一个程序在使用它的时候,它的计数器就会+1,当有进程离开的时候,计数器-1,当计数器为0的时候,系统释放共享内存对象。而在linux下是当共享内存建立,就会一直存在,直到有一个进程给它释放关闭的命令,它才会释放。
有了这些API,在上面我手动封装了一个AccessObject的类,负责创建,打开,映射共享内存地址。
#include "ShareMemoryAPI.h"
class CSMAccessObject
{
public:
CSMAccessObject(void);
~CSMAccessObject(void);
//创建对象,创建的内存为实际内存+头描述
bool Create(SMKey key, int nSize, int nHeadSize);
//检测对象是否存在,如果存在则不再创建,直接打开
bool Open(SMKey key, int nSize, int nHeadSize);
//删除对象
void Destory();
//得到指定长度偏移的指针位置
char* GetData(int nSize);
//得到头描述的位置
char* GetHeadData();
//得到此对象的总长度
int GetDataSize();
//得到此对象的头总长度
int GetHeadDataSize();
private:
SMHandle m_SMHandle;
SMKey m_Key;
int m_nSize;
int m_nHeadSize;
char* m_pHeadData;
char* m_pData;
};
这是一个指定的共享内存对象,这里做了一个m_pHeadData和m_pData两个指针对象,m_pHeadData用于记录此共享内存的内存分配信息(下面会说明这段内存存的是什么),而m_pData记载的是共享内存头的指针位置。比如,当我申请一个内存的时候,实际是m_nHeadSize + m_nSize。一个头信息块+要使用的内存大小。
有了这些,我就可以封装一个共享内存的模板了。因为大多数时候,我需要一些不同的共享内存对象,最简单的方法就是提供类似new和delete的支持功能,就像C++本身那样,可以随意的申请和"释放"(这里的释放不是真的释放,只是标记一下)。
本着这个原则,我创建了一个模板类。
#ifndef _SMPOOL_H
#define _SMPOOL_H
#include "SMAccessObject.h"
#include "Serial.h"
#include <map>
using namespace std;
template<typename T>
class CSMPool
{
public:
CSMPool()
{
m_pSMAccessObject = NULL;
};
~CSMPool()
{
if(m_pSMAccessObject != NULL)
{
delete m_pSMAccessObject;
m_pSMAccessObject = NULL;
}
};
bool Init(SMKey key, int nMaxCount)
{
bool blRet = false;
//初始化序列化头
m_Serial.Init(nMaxCount*sizeof(_SMBlock));
m_pSMAccessObject = new CSMAccessObject();
if(NULL == m_pSMAccessObject)
{
return false;
}
//首先尝试打开,看看是否存在已有,如果不存在,则创建新的。
blRet = m_pSMAccessObject->Open(key, nMaxCount*sizeof(T), nMaxCount*sizeof(_SMBlock));
if(false == blRet)
{
printf("[Init]Create.\n");
//如果没有,则新建。
blRet = m_pSMAccessObject->Create(key, nMaxCount*sizeof(T), nMaxCount*sizeof(_SMBlock));
if(false == blRet)
{
return false;
}
//开始划分共享内存空间
m_mapSMBlock.clear();
m_mapFreeSMBlock.clear();
m_mapUsedSMBlock.clear();
for(int i = 0; i < nMaxCount; i++)
{
_SMBlock* pSMBlock = new _SMBlock();
if(pSMBlock == NULL)
{
return false;
}
pSMBlock->m_nID = i;
pSMBlock->m_pT = reinterpret_cast<T*>(m_pSMAccessObject->GetData(i*sizeof(T)));
if(NULL == pSMBlock->m_pT)
{
return false;
}
m_mapSMBlock.insert(typename mapSMBlock::value_type(i, pSMBlock));
m_mapFreeSMBlock.insert(typename mapFreeSMBlock::value_type(i, pSMBlock));
m_Serial.Serial(pSMBlock, sizeof(_SMBlock));
}
m_nMaxCount = nMaxCount;
//写入消息头
WriteSMHead();
}
else
{
printf("[Init]Open.\n");
//如果存在则读取
char* pHeadData = m_pSMAccessObject->GetHeadData();
if(NULL == pHeadData)
{
return false;
}
//printf("[Init]Open pHeadData = 0x%08x.\n", pHeadData);
//读取共享内存中关于数据的判定
m_Serial.Serial(pHeadData, nMaxCount*sizeof(_SMBlock));
//还原回vector对象
m_mapSMBlock.clear();
m_mapFreeSMBlock.clear();
m_mapUsedSMBlock.clear();
for(int i = 0; i < nMaxCount; i++)
{
_SMBlock* pSMBlock = new _SMBlock();
if(pSMBlock == NULL)
{
return false;
}
m_Serial.GetData(pSMBlock, sizeof(_SMBlock));
pSMBlock->m_pT = reinterpret_cast<T*>(m_pSMAccessObject->GetData(i*sizeof(T)));
m_mapSMBlock.insert(typename mapSMBlock::value_type(pSMBlock->m_nID, pSMBlock));
//分类
if(pSMBlock->m_blUse == true)
{
//放入正在使用的列表
m_mapUsedSMBlock.insert(typename mapUsedSMBlock::value_type(pSMBlock->m_pT, pSMBlock));
}
else
{
//放入没有使用的列表
m_mapFreeSMBlock.insert(typename mapFreeSMBlock::value_type(i, pSMBlock));
}
}
m_nMaxCount = nMaxCount;
}
return true;
};
void Close()
{
typename mapSMBlock::iterator b = m_mapSMBlock.begin();
typename mapSMBlock::iterator e = m_mapSMBlock.end();
for(b; b != e; b++)
{
_SMBlock* pSMBlock = (_SMBlock* )b->second;
if(pSMBlock != NULL)
{
delete pSMBlock;
}
}
m_mapSMBlock.clear();
m_pSMAccessObject->Destory();
}
T* NewObject()
{
//在m_mapSMBlock中查找一个空余的对象。
if(m_mapFreeSMBlock.size() > 0)
{
typename mapFreeSMBlock::iterator b = m_mapFreeSMBlock.begin();
_SMBlock* pSMBlock = (_SMBlock* )b->second;
if(NULL == pSMBlock)
{
return NULL;
}
else
{
m_mapFreeSMBlock.erase(b);
pSMBlock->m_blUse = true;
m_mapUsedSMBlock.insert(typename mapUsedSMBlock::value_type(pSMBlock->m_pT, pSMBlock));
m_Serial.WriteData(pSMBlock->m_nID*sizeof(_SMBlock), pSMBlock, sizeof(_SMBlock));
//写入消息头
WriteSMHead();
return pSMBlock->m_pT;
}
}
else
{
return NULL;
}
};
bool DeleteObject(T* pData)
{
typename mapUsedSMBlock::iterator f = m_mapUsedSMBlock.find(pData);
if(f == m_mapUsedSMBlock.end())
{
return false;
}
else
{
_SMBlock* pSMBlock = (_SMBlock* )f->second;
m_mapUsedSMBlock.erase(f);
pSMBlock->m_blUse = false;
m_mapFreeSMBlock.insert(typename mapFreeSMBlock::value_type(pSMBlock->m_nID, pSMBlock));
m_Serial.WriteData(pSMBlock->m_nID*sizeof(_SMBlock), pSMBlock, sizeof(_SMBlock));
//写入消息头
WriteSMHead();
return true;
}
};
int GetFreeObjectCount()
{
return (int)m_mapFreeSMBlock.size();
}
int GetUsedObjectCount()
{
return (int)m_mapUsedSMBlock.size();
};
T* GetUsedObject(int nIndex)
{
if(nIndex >= (int)m_mapUsedSMBlock.size())
{
return NULL;
}
typename mapUsedSMBlock::iterator b = m_mapUsedSMBlock.begin();
typename mapUsedSMBlock::iterator e = m_mapUsedSMBlock.end();
int nPos = 0;
for(b; b != e; b++)
{
if(nPos == nIndex)
{
_SMBlock* pSMBlock = (_SMBlock* )b->second;
if(NULL != pSMBlock)
{
return pSMBlock->m_pT;
}
else
{
return NULL;
}
}
else
{
if(nPos > nIndex)
{
return NULL;
}
nPos++;
}
}
}
private:
void WriteSMHead()
{
char* pHeadData = m_pSMAccessObject->GetHeadData();
if(NULL == pHeadData)
{
return;
}
//将头信息写入共享内存头
memcpy(pHeadData, m_Serial.GetBase(), m_nMaxCount*sizeof(_SMBlock));
};
private:
//记录每个队列的数据容器
struct _SMBlock
{
T* m_pT;
int m_nID;
bool m_blUse;
_SMBlock()
{
m_pT = NULL;
m_nID = 0;
m_blUse = false;
}
};
typedef map<int, _SMBlock*> mapSMBlock;
typedef map<T*, _SMBlock*> mapUsedSMBlock;
typedef map<int, _SMBlock*> mapFreeSMBlock;
private:
CSMAccessObject* m_pSMAccessObject;
mapSMBlock m_mapSMBlock;
mapUsedSMBlock m_mapUsedSMBlock;
mapFreeSMBlock m_mapFreeSMBlock;
CSerial m_Serial;
int m_nMaxCount;
};
#endif
代码有点多,实际多的那部分,是关于头标记的部分。
因为很多时候,程序会因为各种原因而崩溃。而再次重启的时候,可能有些共享内存对象已经被分配出去了。这时候如果我再new出来,可能就会覆盖原有的数据,所以我把所有的内存块都存成一个数据结构,放在共享内存的头里面,当下次程序启动,我会预读这个头,并还原内存使用列表,最大限度的保证数据获取与写入的正确。
使用的话么,呵呵,其实很简单。
struct _Data //一个测试的对象
{
int m_nData;
char m_szData[20];
_Data()
{
m_nData = 0;
m_szData[0] = '\0';
}
};
#ifdef _WIN32
int _tmain(int argc, _TCHAR* argv[])
#else
int main(int argc, char* argv[])
#endif
{
CSMPool<_Data> UserPool;
SMKey key;
#ifdef _WIN32
key = 1111;
#else
key = ftok("/home/freeeyes/SMData/", 0);
#endif
printf("[Test]%d.\n", key);
UserPool.Init(key, 100);
_Data* pData1 = UserPool.NewObject();
if(NULL == pData1)
{
printf("[main]Get Data1 NULL.\n");
}
else
{
pData1->m_nData = 1;
sprintf(pData1->m_szData, "shiqiang");
printf("[main]Free Count = %d.\n", UserPool.GetFreeObjectCount());
printf("[main]Get Data1 OK[%d] - [%s].\n", pData1->m_nData, pData1->m_szData);
}
}
呵呵,一个很简单使用共享内存的例子。
其实,要想真正把共享内存做的更完美一些,需要一些手段,下一讲我将着重介绍如何使用共享内存的一些简单策略,比如如何防止不同的进程同时写。数据的合理化规划等等,希望抛砖引玉,大家需要用到的时候,这篇文章可以给你帮助。
以上代码在VS2005下和64位linux运行测试通过。(可以修改我的Make文件,一般直接make即可编译)