簡單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

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