在維護公司的一個項目的時候發現了一個共享內存類,看了一下注釋,發現是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