在维护公司的一个项目的时候发现了一个共享内存类,看了一下注释,发现是chrome里头的代码,所以就把chrome的代码翻出来看了一个,果然写的不错,考虑的情况也确实比较多,想想之前看过了《windows核心编程》这本书也有讲,所以就把书中的相关章节又看了一遍,写这篇文章就算是一个总结吧
先上代码:
#include
#include
#include
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(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
#include
#include
#include
#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
2020.5.1更新:
评论区有些网友反映pVoid指针为空,那很可能是因为创建内存映射文件函数CreateFileMappingW调用失败了。
因为在名称中我是这么写的:#define MAP_FILE_NAME (L"Global\\TestName")
把全局命令空间前缀加上了,有些程序可能并没有权限去创建或者打开这样的内存映射文件导致。一般GetLastError会返回5,表示没有权限。解决办法可以把名称的Global\\去掉即可。