freeeyes 发表于 2010-4-21 13:14:54

共享内存实用研究(一)

由于最近一段时间较忙,事情较多,一直没有时间发布新帖。因为以前做过一些游戏服务器的需求,所以希望在这里把自己的一些游戏服务器设计经验一点点共享出来,供大家参考,当然,对于我个人而言,我想借助这些文章,沉淀一下自己的知识,抽象一下以前的代码。当然,可能有的并不完善,欢迎大家提出自己的意见和想法。
由于有些代码是商业的,不能公开,所以我以我的理解重写了,或许有些问题的地方,还请各位指正。
以下代码我都会以附件的形式贴出来。这里只写一些主要的部分。
我觉得共享内存,在服务器应用中是很有用的一个东西,尤其在跨进程访问的时候,比如,我从数据库读取一些用户信息,预先放进共享内存,然后,另外的一个进程通过这个结构去访问这些数据。这样做的好处是,就算其中一个服务器宕机,只要另外的服务器还在运行,共享内存就会得以保存,当宕机的服务器重启之后,能还原到宕机那一时刻的数据。在网络游戏中应用尤其多,但是同样,共享内存是一个双刃剑,难处理的是跨进程的读写操作,容易造成一些领崩溃的问题。不过,这些困难,是可以通过设计减少它的发生概率的。
呵呵,废话少说了,先从最基本的开始吧。
由于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("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("Open.\n");
                        //如果存在则读取
                        char* pHeadData = m_pSMAccessObject->GetHeadData();
                        if(NULL == pHeadData)
                        {
                                return false;
                        }
                        //printf("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;
                intm_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   //一个测试的对象
{
        intm_nData;
        char m_szData;

        _Data()
        {
                m_nData   = 0;
                m_szData = '\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("%d.\n", key);
        UserPool.Init(key, 100);

        _Data* pData1 = UserPool.NewObject();
        if(NULL == pData1)
        {
                printf("Get Data1 NULL.\n");
        }
        else
        {
                pData1->m_nData = 1;
                sprintf(pData1->m_szData, "shiqiang");
                printf("Free Count = %d.\n", UserPool.GetFreeObjectCount());
                printf("Get Data1 OK[%d] - [%s].\n", pData1->m_nData, pData1->m_szData);
        }
}


呵呵,一个很简单使用共享内存的例子。
其实,要想真正把共享内存做的更完美一些,需要一些手段,下一讲我将着重介绍如何使用共享内存的一些简单策略,比如如何防止不同的进程同时写。数据的合理化规划等等,希望抛砖引玉,大家需要用到的时候,这篇文章可以给你帮助。
以上代码在VS2005下和64位linux运行测试通过。(可以修改我的Make文件,一般直接make即可编译)

392119301 发表于 2011-12-14 20:14:06

在没人回复呢,讲解的多好啊。

zhendingl 发表于 2012-1-12 14:33:24

特来学习~~

213yy 发表于 2012-1-27 19:24:08

特来学习~~ 很好,谢谢! , 期待下一节
页: [1]
查看完整版本: 共享内存实用研究(一)