简单windows共享内存封装类

在维护公司的一个项目的时候发现了一个共享内存类,看了一下注释,发现是chrome里头的代码,所以就把chrome的代码翻出来看了一个,果然写的不错,考虑的情况也确实比较多,想想之前看过了《windows核心编程》这本书也有讲,所以就把书中的相关章节又看了一遍,写这篇文章就算是一个总结吧

先上代码:


#include <Windows.h>
#include <string>
#include <process.h>


class SharedMemory
{
public:
    SharedMemory(BOOL bReadOnly = FALSE) : m_hLock(NULL),
        m_hFileMap(NULL),
        m_pMemory(NULL),
        m_bReadOnly(FALSE),
        m_dwMappedSize(0),
        m_strName(L"")
    {
    
    }

    BOOL Create(const std::wstring& strName, DWORD dwSize)
    {
        if (dwSize <= 0)
            return FALSE;

        HANDLE handle = ::CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, dwSize, strName.empty() ? NULL : strName.c_str());
        if (!handle)
            return FALSE;

        // 已经存在了
        if (GetLastError() == ERROR_ALREADY_EXISTS)
        {
            Close();
            return FALSE;
        }

        m_hFileMap = handle;
        m_dwMappedSize = dwSize;
        return TRUE;
    }

    BOOL Open(const std::wstring& strName, BOOL bReadOnly)
    {
        m_hFileMap = ::OpenFileMappingW(bReadOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, FALSE, strName.empty() ? NULL : strName.c_str());
        if (!m_hFileMap)
            return FALSE;

        m_bReadOnly = bReadOnly;
        return TRUE;
    }

    BOOL MapAt(DWORD dwOffset, DWORD dwSize)
    {
        if (!m_hFileMap)
            return FALSE;

        if (dwSize > ULONG_MAX)
            return FALSE;

        ULARGE_INTEGER ui;
        ui.QuadPart = static_cast<ULONGLONG>(dwOffset);

        m_pMemory = ::MapViewOfFile(m_hFileMap,
            m_bReadOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, ui.HighPart, ui.LowPart, dwSize);

        return ( m_pMemory != NULL );
    }

    void Unmap()
    {
        if (m_pMemory)
        {
            ::UnmapViewOfFile(m_pMemory);
            m_pMemory = NULL;
        }
    }

    LPVOID GetMemory() const { return m_pMemory; }

    HANDLE GetHandle() const
    {
        return m_hFileMap;
    }

    // 锁定共享内存
    BOOL Lock(DWORD dwTime)
    {
        // 如果还没有创建锁就先创建一个
        if (!m_hLock)
        {
            std::wstring strLockName = m_strName;
            strLockName.append(L"_Lock");
            // 初始化的时候不被任何线程占用
            m_hLock = ::CreateMutexW(NULL, FALSE, strLockName.c_str());
            if (!m_hLock)
                return FALSE;
        }

        // 哪个线程最先调用等待函数就最先占用这个互斥量
        DWORD dwRet = ::WaitForSingleObject(m_hLock, dwTime);
        return (dwRet == WAIT_OBJECT_0 || dwRet == WAIT_ABANDONED);
    }

    void Unlock()
    {
        if (m_hLock)
        {
            ::ReleaseMutex(m_hLock);
        }
    }

    SharedMemory::~SharedMemory() 
    {
        Close();
        if (m_hLock != NULL)
        {
            CloseHandle(m_hLock);
        }
    }

    void Close()
    {
        Unmap();

        if (m_hFileMap)
        {
            ::CloseHandle(m_hFileMap);
            m_hFileMap = NULL;
        }
    }

private:
    HANDLE m_hLock;
    HANDLE m_hFileMap;
    LPVOID m_pMemory;
    std::wstring m_strName;
    BOOL m_bReadOnly;
    DWORD m_dwMappedSize;

    SharedMemory(const SharedMemory& other);
    SharedMemory& operator = (const SharedMemory& other);
};


共享内存的原理:

共享内存其实是一种特殊的文件映射对象,而文件映射对象本质上又是虚拟内存(这个知识点可以看《windows核心编程》或者在网上找资料了解学习一下)。

虚拟内存一般是通过页交换文件来实现的,这个页交换文件是什么呢?一般就是我们C盘中的pagefile.sys文件,如下图所示:




不过也有通过文件来实现的,比如我们双击exe文件,这时其实是用这个exe文件本身来作为虚拟内存来使用(参见《windows核心编程》中的讲解)。

而内存映射文件实现原理也就是通过文件来实现虚拟内存的,实现原理和双击exe的原理是类似的。这个文件基本上可以是任意文件,打开之后作为虚拟内存映射到进程

的地址空间中。

我们先来大致看一下虚拟内存的使用过程吧:

1. 在进程地址空间中预订区域(VirtualAlloc)

2. 给区域调拨物理存储器(VirtualAlloc)

3. 使用虚拟内存(memcpy等函数)

4. 撤销调拨物理存储器,释放所预订的区域(VirtualFree)

这里所说的物理存储器一般是指页交换文件(pagefile.sys)


我们再来看一下内存映射文件的使用过程:

1. 打开或者创建文件(CreateFile)

2. 创建文件映射对象(CreateFileMapping)

3. 映射文件视图(MapViewOfFile)

4. 使用内存映射文件(memcpy等函数)

5. 撤销文件视图(UnMapViewOfFile)

6. 关闭映射文件句柄(CloseHandle)


之前说内存映射文件本质上是虚拟内存,那么这两个过程又是怎么对应的呢?

其实这两个过程基本上是一一对应的,只不过在上层使用的时候我们感觉不到而已。

打开文件或创建文件可以看成是在准备虚拟内存的物理存储器。

创建文件映射对象可以看成是预订区域

映射文件试图可以看成是给区域调拨物理存储器,这个物理存储器就是之前打开或者创建的文件

撤销文件视图可以看成是撤销调拨物理存储器

关闭映射文件句柄可以看成是释放区域

整个过程基本上就是这样。


一个普通的文件映射对象的使用代码大致是这样子:

HANDLE hFile = CreateFile(...)
HANDLE hFileMap = CreateFileMapping(hFile, ...);
PVOID pView = MapViewOfFile(hFileMap,FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
memcpy(pView, ...);
UnmapViewOfFile(pView);
CloseHandle(hFileMap);
CloseHandle(hFile);

而共享内存的代码有什么不同呢?

主要区别是物理存储器不一样,普通的内存映射文件都是使用磁盘上的文件作为物理存储器,而共享内存使用的是也交换文件(pagefile.sys)。

这个区别如何体现的代码上呢?就是在调用函数CreateFileMapping的时候第一个参数是INVALID_HANDLE_VALUE。

整个过程基本上讲解完了,下面来分析一下SharedMemory这个类。


我们要在不同的进程中进行共享,那么我们需要创建一个命名的对象(其他进程间通信方式有:粘贴板,Socket,WM_COPY消息,邮槽,管道等)。

还有一个问题是需要在不同线程之间进行同步,否则数据有可能会乱套。这里使用了一个互斥量。

现在顺便问一个问题,能用关键段吗?

不能,因为关键段不能跨进程使用,而且这种场合使用需要一个等待时间,关键段也是不支持的。

至于关键段和互斥量的具体区别也可以参见《windows核心编程》里头的讲解。

代码中的Lock函数会让第一个调用的线程占有互斥量,第二个调用者等待,用起来也是比较方便。


代码应该不用过多解释吧,下面来一下使用示例:


#include <Windows.h>
#include <string>
#include <process.h>
#include <tchar.h>

#define MAP_FILE_NAME (L"Global\\TestName")
#define MAP_SIZE (4*1024)
#define STRING_BUF (L"helloword")

UINT __stdcall ThreadFunc(LPVOID lParam)
{
    SharedMemory *pSharedMemory = NULL;
    pSharedMemory = new SharedMemory();
    pSharedMemory->Lock(INFINITE);
    pSharedMemory->Open(MAP_FILE_NAME, FALSE);
    pSharedMemory->MapAt(0, MAP_SIZE);
    LPVOID pVoid = pSharedMemory->GetMemory();
    int length = wcslen(STRING_BUF);
    WCHAR *pbuf = new WCHAR[length + 1];
    
    memcpy(pbuf, pVoid, length*2);
    pbuf[length] = L'\0';

    pSharedMemory->Unlock();
    pSharedMemory->Close();
    delete[] pbuf;
    delete pSharedMemory;
    int i = 0;
    while(true)
        Sleep(5000);
    return 1;
}

int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nShowCmd )
{
    SharedMemory *pSharedMemory = NULL;
    pSharedMemory = new SharedMemory();
    pSharedMemory->Lock(INFINITE);
    pSharedMemory->Create(MAP_FILE_NAME, MAP_SIZE);
    pSharedMemory->MapAt(0, MAP_SIZE);
    LPVOID pVoid = pSharedMemory->GetMemory();
    wcscpy((WCHAR*)pVoid, STRING_BUF);
    pSharedMemory->Unlock();

    _beginthreadex(NULL, 0, ThreadFunc, NULL, NULL, 0);

     Sleep(30000);

    delete pSharedMemory;
    
    return 0;
}

这里是在同一个进程的不同线程来测试的,用不同的进程也是没有问题的。

每个线程在使用的时候一定要加锁解锁,否则就有可能出现数据不一致的问题。

这个过程又一个很重要的问题,就是创建共享之后,不要调用Close函数把共享内存关闭了,这个过程在虚拟内存中相当于

是否了申请的区域,如果这样的话其他进程就不能使用了,OpneFileMapping会失败,也就是不能共享数据了。

之前发现OpenFileMapping失败了搞了半天才明白过来,看来之前对内存映射文件的理解还是不够深刻-_-

注意,释放pSharedMemory的时候也调用了Close函数,所以要保证其他进程在通信过程中不要释放pSharedMemory指针。


参考资料:

1. 《windows核心编程》

2. chrome源码中SharedMemory类的代码(在src\\base\\memory路径下)

3. http://www.cnblogs.com/kex1n/archive/2011/08/10/2133389.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章