本文章先介绍共享内存是啥,再介绍使用共享内存要涉及的4个函数,最后将实例代码附上,实例代码用于加深理解,读者也可以模仿用例改编使用。
1. 共享内存的简单介绍
共享内存就是允许两个不相关的进程访问同一个虚拟内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
共享内存,存在于每个进程的进程地址空间中,属于每个进程,由于它并不需要系统调用干预和数据复制,它的效率是非常高的,它比我们所学的几种IPC机制(信号量,管道,消息队列)都要快。虽然它性能最好,但是它不提供同步互斥机制,因此需要我们程序员来提供,带来了编程的难度。这也是其他IPC机制存在的原因~
总结以上我们可以得出下面3点:
1)共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝。
2)为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。
3)由于多个进程共享一段内存,因此也需要依靠某种同步机制
2. 共享内存涉及的函数
使用共享内存主要用到四个函数。下面是这些函数的原型和使用说明。
1)CreateFileMapping
函数功能:创建一个新的文件映射内核对象。即创建内存映射文件,将上述真正存在的物理文件映射成为一个虚拟的映射文件,就是把物理文件与虚拟文件绑定。
函数原型:
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);
参数说明:
hFile:任何可以获得的物理文件句柄。
a)如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为 0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了。使用 INVALID_HANDLE_VALUE还需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否有效, 这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围. 返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0。
b) 如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和"保护设置"匹配, 比如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建。(调用CreateFile函数创建,将其返回值赋值给hFile即可)
lpAttributes:安全设置,一般设置NULL就可以了, 使用默认的安全配置。
flProtect:当文件映射时读写文件的属性。其值为下述常数之一:
PAGE_READONLY | 以只读方式打开映射 |
PAGE_READWRITE | 以可读、可写方式打开映射 |
PAGE_WRITECOPY | 为写操作留下备份 |
或组合使用下述一个或多个常数:
SEC_COMMIT | 为文件映射一个小节中的所有页分配内存 |
SEC_IMAGE | 文件是个可执行文件 |
SEC_RESERVE | 为没有分配实际内存的一个小节保留虚拟内存空间 |
PS:(⊙o⊙)…第二个表格的几个常数没用过,好像也不常用。我不太懂...
dwMaximumSizeHigh:文件映射的最大长度的高32位。(这个应该是与申请的内存空间大小相关)
dwMaximumSizeLow:文件映射的最大长度的低32位。如这个参数和dwMaximumSizeHigh都是零,就用磁盘文件的实际长度。一般将dwMaximumSizeHigh设置为0,dwMaximumSizeLow设置为我们想开辟的内存字节数。
lpName:指定文件映射对象的名字。如存在这个名字的一个映射,函数就会打开它。若为空,则创建一个无名的文件映射。
函数返回值:
a)如果函数成功,则返回值是新创建的文件映射对象的句柄。
b)如果在函数调用之前存在该对象,则该函数返回现有对象的句柄(具有当前大小,而不是指定大小),并且GetLastError返回ERROR_ALREADY_EXISTS。
c)如果函数失败,则返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
调用CreateFileMapping的时候可能会出现的GetLastError的相应错误:
a)ERROR_FILE_INVALID (错误_文件_无效)如果企图创建一个零长度的文件映射
b)ERROR_INVALID_HANDLE(错误_无效_处理) 内存空间的命名和现有的内存映射,互斥量,信号量,临界区有同名
c)ERROR_ALREADY_EXISTS (错误或已经存在)表示内存空间命名已经存在
2)MapViewOfFile
函数功能:将共享内存映射到进程的地址空间。(个人理解是将磁盘文件映射到内存中)
函数原型:
LPVOID WINAPI MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap
);
参数说明:
hFileMappingObject:文件映射对象的句柄。其值为CreateFileMapping和OpenFileMapping函数的返回值。
dwDesiredAccess:访问文件映射对象的类型(权限)。要与在CreateFileMapping()时设置的访问权限相匹配。
a)FILE_MAP_ALL_ACCESS等价于CreateFileMapping的FILE_MAP_WRITE | FILE_MAP_READ. 文件映射对象被创建时必须指定PAGE_READWRITE选项.
b)FILE_MAP_COPY可以读取和写入文件.写入操作会导致系统为该页面创建一份副本.在调用CreateFileMapping时必须传入PAGE_WRITECOPY保护属性.
dwFileOffsetHigh和dwFileOffsetLow:文件映射起始偏移的高32位和低32位,用来告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节,通常情况都设置为0。
dwNumberOfBytesToMap:指定需要映射的文件的字节数量,当该值为0,则映射的是整个文件 。
函数返回值:
如果函数成功,则返回值是映射视图的起始地址。
如果函数失败,则返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
3)OpenFileMapping
函数功能:打开共享内存,用于打开一个现成的文件映射对象。
函数原型:
HANDLE WINAPI OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName
);
参数说明:
dwDesiredAccess:访问文件映射对象的类型(权限)。要与在CreateFileMapping()时设置的访问权限相匹配。
a)FILE_MAP_ALL_ACCESS等价于CreateFileMapping的FILE_MAP_WRITE | FILE_MAP_READ. 文件映射对象被创建时必须指定PAGE_READWRITE选项.
b)FILE_MAP_COPY可以读取和写入文件.写入操作会导致系统为该页面创建一份副本.在调用CreateFileMapping时必须传入PAGE_WRITECOPY保护属性.
bInheritHandle:如果此参数为TRUE,CreateProcess函数创建的进程可以继承该句柄; 否则,句柄不能被继承,一般情况下我们设置为FALSE。
lpName:要打开的文件映射对象的名称,也就是CreateFileMapping时的lpName。
函数返回值:
如果函数成功,则返回值是指定文件映射对象的打开句柄。
如果函数失败,则返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
4)UnmapViewOfFile
函数功能:从调用进程的地址空间取消映射文件的映射视图。用于释放内存区域。
函数原型:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);
参数说明:
lpBaseAddress:此值必须与上一次调用MapViewOfFile函数返回的值相同,其指向要取消映射的文件的映射视图的基址的指针。
函数返回值:
成功返回非零值,失败返回零,可调用GetLastError查看错误信息。
3. 代码示例
该代码示例涉及3个进程共同读写一块共享内存,因此要结合上一篇说的互斥量实现进程通信。进程A等待读信息,进程B和进程C写信息,在进程A读取进程B发送的信息后,进程C往共享内存写内容供进程A再次读取。因此进程A在读取信息后要将共享内存内容清零,方便下次读取。
实现流程:
·创建共享内存的进程
Step1:当调用 CreateFileMapping 创建命名的内存映射文件对象时,Windows 即在物理内存申请一块指定大小的内存区域,返回文件映射对象的句柄 hMap。只要创建共享内存的进程没有关闭句柄hMap,以后运行的程序就会读出共享内存里面的数据
Step2:为了能够访问这块内存区域必须调用 MapViewOfFile 函数,促使 Windows 将此内存空间映射到进程的地址空间中。
·访问共享内存的进程
当在其他进程访问这块内存区域时,则必须使用OpenFileMapping 函数取得对象句柄 hMap,并调用 MapViewOfFile 函数得到此内存空间的一个映射。
这样一来,系统就把同一块内存区域映射到了不同进程的地址空间中,从而达到共享内存的目的。
进程A:
#include "stdafx.h"
#include "SharedMemory.h"
#include "Mutex.h"
#include <iostream>
using namespace std;
const CString g_strMutexName = L"MutexName_test";
const CString g_strMutexName1 = L"MutexName_test1";
const CString g_strSMName = L"SharedMemoryName_test";
int _tmain(int argc, _TCHAR* argv[])
{
CMutex mutex(g_strMutexName);
CMutex mutex1(g_strMutexName1);
CSharedMemory sharedMemory(g_strSMName);
if (!sharedMemory.Create())
return -1;
char chTest[1024];
sharedMemory.Map();
// 第一个进程发的信息
cout<<"准备进入共享区域...\n";
mutex.Lock();
cout<<"准备读...\n";
// 不加这个会出现这样的情况:一时能将共享内存内容读出来,一时读不出来
// 猜测原因是写进去也要时间?
Sleep(10);
sharedMemory.Read(chTest, sizeof(chTest));
cout<<"读到的结果为:";
printf(chTest);
sharedMemory.Clear(sizeof(chTest));
mutex.UnLock();
mutex.Close();
// 第二个进程发的信息
cout<<"\n准备进入共享区域...\n";
mutex1.Lock();
int k;
cout<<"准备读...\n";
sharedMemory.Read(&k, sizeof(k));
cout<<"读到的结果为:"<<k;
mutex1.UnLock();
mutex1.Close();
sharedMemory.Unmap();
sharedMemory.Close();
system("pause");
return 0;
}
进程B:
#include "stdafx.h"
#include "Mutex.h"
#include "SharedMemory.h"
#include <iostream>
using namespace std;
const CString g_strMutexName = L"MutexName_test";
const CString g_strSMName = L"SharedMemoryName_test";
int _tmain(int argc, _TCHAR* argv[])
{
CMutex mutex(g_strMutexName);
CSharedMemory sharedMemory(g_strSMName);
if (!mutex.Create())
return -1;
sharedMemory.Open();
sharedMemory.Map();
cout<<"正在写...\n";
char chSrc[]="this is test";
int i = sizeof(chSrc);
cout<<i;
sharedMemory.Write(chSrc, sizeof(chSrc));
cout<<"已写完...\n";
mutex.UnLock();
sharedMemory.Unmap();
return 0;
}
进程C:
#include "stdafx.h"
#include <iostream>
#include "Mutex.h"
#include "SharedMemory.h"
using namespace std;
const CString g_strMutexName1 = L"MutexName_test1";
const CString g_strSMName = L"SharedMemoryName_test";
int _tmain(int argc, _TCHAR* argv[])
{
CMutex mutex(g_strMutexName1);
CSharedMemory sharedMemory(g_strSMName);
sharedMemory.Open();
sharedMemory.Map();
int k=1000;
mutex.Create();
sharedMemory.Write(&k, sizeof(k));
sharedMemory.Unmap();
mutex.UnLock();
return 0;
}
CMutex类:
CSharedMemory类:
后记:
这段话纯属个人牢骚,可忽略~写这几个exe是为了巩固多个进程通过共享内存进行通信理解,因为该内存是多个进程共同享有的,所以共享内存要配合互斥量的使用。于是先写了一篇文章关于互斥量的使用,在理解完互斥量后就开始着笔写这篇文章,感想1:先理解共享内存要用到的几个函数的作用,参数后再开始写代码简单多了。感想2:将互斥量和共享内存的使用封装成类方便多个工程使用,感受到了c++面向对象的魅力~感想3:写博客总结真的可以加深理解,即使这种理解是暂时记忆,但是后期自己遇到同样问题再查看自己写的相关博客可以唤醒记忆!即使没有唤醒相关记忆,由于自己写的博客是按自己容易接受学习的方式书写的,所以再次阅读重新学习时也会比较快入手!٩(๑❛ᴗ❛๑)۶
若该博客让你有收获,可以点个赞哟(❁´ω`❁)
以上是本博客的所有内容,若有改进之处,欢迎指正~ヾ(❀^ω^)ノ゙