一、原理
简单来说,共享内存的方式,就是在内存中开辟一块区域,专门用来存放进程需要交换的消息。当内存中已有被A进程写入了消息,那么其他进程就可以在此内存中读取到A进程想分享的消息了。关于内存的操作,Kernel32.dll为我们提供了便捷的函数接口。
1.CreateFileMapping
创建文件映射,返回值是创建的文件的句柄,该文件也就是共享的内存。
[DllImport("Kernel32.dll", EntryPoint = "CreateFileMapping")] public static extern IntPtr CreateFileMapping( IntPtr hFile,//物理文件句柄 UInt32 lpAttributes,//安全设置 UInt32 flProtect,//保护设置 UInt32 dwMaximumSizeHigh,//高位文件大小 UInt32 dwMaximumSizeLow,//低位文件大小 string lpName//共享内存名称 );
(1) 物理文件句柄
任何可以获得的物理文件句柄, 如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为 0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了.
如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和"保护设置"匹配, 比如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建.
如果使用 INVALID_HANDLE_VALUE, 也需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否有效, 这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围. 返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.
(2) 保护设置
lpFileMappigAttributes:SECURITY_ATTRIBUTES,它指明返回的句柄是否可以被子进程所继承,指定一个安全对象,在创建文件映射时使用。如果为NULL(用ByVal As Long传递零),表示使用默认安全对象。
在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用是, 可以考虑进行限制.
(4) 高位文件大小
弟兄们, 我想目前我们的机器都是32位的东东, 不可能得到超过32位进程所能寻址的私有32位地址空间, 一般还是设置0吧, 我没有也不想尝试将它设置超过0的情况.
(5) 低位文件大小
这个还是可以进行设置的, 不过为了让其他共享用户知道你申请的文件映射的相关信息, 我使用的时候是在获得的地址空间头部添加一个结构化描述信息, 记录内存映射的大小, 名称等, 这样实际申请的空间就比输入的增加了一个头信息结构大小了, 我认为这样类似BSTR的方式应该是比较合理的.
(6) 共享内存名称
指定文件映射对象的名字。如存在这个名字的一个映射,函数就会打开它。用vbNullString可以创建一个无名的文件映射。
另外,调用CreateFileMapping的时候GetLastError的对应错误
ERROR_FILE_INVALID 如果企图创建一个零长度的文件映射, 应有此报
ERROR_INVALID_HANDLE 如果发现你的命名内存空间和现有的内存映射, 互斥量, 信号量, 临界区同名就麻烦了
ERROR_ALREADY_EXISTS 表示内存空间命名已经存在
2.OpenFileMapping
根据共享内存名称来打开文件映射对象,返回值是指定文件映射对象的句柄。
[DllImport("Kernel32.dll", EntryPoint = "OpenFileMapping")] public static extern IntPtr OpenFileMapping( UInt32 dwDesireAccess,//DWORD dwDesiredAccess, int bInheritHandle,//BOOL bInheritHandle string lpName//LPCTSTR lpName );
(1)dwDesiredAccess Long,带有前缀FILE_MAP_???的一个常数。参考MapViewOfFile函数的dwDesiredAccess
(2)bInheritHandle Long,如这个函数返回的句柄能由当前进程启动的新进程继承,则这个参数为TRUE
(3)lpName String,指定要打开的文件映射对象名称
3.MapViewOfFile 映射缓冲区视图,即将文件映射视图映射到调用进程的地址空间。
[DllImport("Kernel32.dll", EntryPoint = "MapViewOfFile")] public static extern IntPtr MapViewOfFile( IntPtr hFileMappingObject,//HANDLE hFileMappingObject, UInt32 dwDesiredAccess,//DWORD dwDesiredAccess UInt32 dwFileOffsetHight,//DWORD dwFileOffsetHigh, UInt32 dwFileOffsetLow,//DWORD dwFileOffsetLow, UInt32 dwNumberOfBytesToMap//SIZE_T dwNumberOfBytesToMap );
如果函数成功,则返回值是映射视图的起始地址。
如果函数失败,则返回值为NULL。要获取扩展错误信息,请调用GetLastError。
(1)hFileMappingObject
文件映射对象的句柄。CreateFileMapping和 OpenFileMapping函数返回该句柄。
(2)dwDesiredAccess
对文件映射对象的访问类型,它决定了页面的页面保护。此参数可以是以下值之一,或者在适当的情况下是多个值的按位 OR 组合。
①FILE_MAP_ALL_ACCESS
文件的读/写视图被映射。文件映射对象必须是使用PAGE_READWRITE或PAGE_EXECUTE_READWRITE保护创建的 。
当与MapViewOfFile函数一起使用时, FILE_MAP_ALL_ACCESS等效于FILE_MAP_WRITE。
②FILE_MAP_READ
文件的只读视图被映射。尝试写入文件视图会导致访问冲突。
文件映射对象必须是使用PAGE_READONLY、 PAGE_READWRITE、PAGE_EXECUTE_READ或 PAGE_EXECUTE_READWRITE保护创建的。
③FILE_MAP_WRITE
文件的读/写视图被映射。文件映射对象必须是使用PAGE_READWRITE或PAGE_EXECUTE_READWRITE保护创建的 。
当与MapViewOfFile 一起使用时, ( FILE_MAP_WRITE | FILE_MAP_READ ) 和 FILE_MAP_ALL_ACCESS等价于FILE_MAP_WRITE。
(3)dwFileOffsetHigh
视图开始处的文件偏移量的高阶DWORD。
(4)dwFileOffsetLow
视图开始位置的文件偏移量的低位DWORD。高偏移和低偏移的组合必须指定文件映射内的偏移。它们还必须与系统的内存分配粒度相匹配。也就是说,偏移量必须是分配粒度的倍数。要获取系统的内存分配粒度,请使用 GetSystemInfo函数,该函数填充SYSTEM_INFO结构的成员。
(5)dwNumberOfBytesToMap
映射到视图的文件映射的字节数。所有字节都必须在CreateFileMapping指定的最大大小范围内。如果此参数为 0(零),则映射从指定的偏移量扩展到文件映射的末尾。
4.预定义的常量
public const int INVALID_HANDLE_VALUE = -1; public const int PAGE_READWRITE = 0x04; public const int FILE_MAP_ALL_ACCESS = 0x0002; public const int FILE_MAP_WRITE = 0x0002;
5.进程安全
我们惊奇的发现,这种运作模式与生产者消费者模式极其相似,因此在进行共享内存读写操作时,要设置读写信号量,并利用这些读写信号量实现共享内存区域的进程安全。
private Semaphore m_Write;//可写的信号 private Semaphore m_Read;//可读的信号
操作系统中的PV操作,在这里就是WaitOne与Release。其中,信号量Semaphore构造函数如下:
// // 摘要: // 初始化 System.Threading.Semaphore 类的新实例,并指定最大并发入口数,还可以选择为调用线程保留某些入口,以及选择指定系统信号量对象的名称。 // // 参数: // initialCount: // 可以同时授予的信号量的初始请求数。因初始时共享内存无内容,故无法进行读操作,读操作此初始值为0,而可以写入,写操作此初始值为1; // // maximumCount: // 可以同时授予的信号量的最大请求数。读写均为互斥操作,即同一时刻最多只能有一个读或者写,因此此初始值均为1 // // name: // 命名系统信号量对象的名称。可以将其看成唯一标识符,进程间同步可以通过此name寻找信号量。 // public Semaphore(int initialCount, int maximumCount, string name);
基本操作如下
//声明读写信号量 m_Write = new Semaphore(1, 1, "WriteMap"); m_Read = new Semaphore(0, 1, "ReadMap"); //获取信号量 m_Write = Semaphore.OpenExisting("WriteMap"); m_Read = Semaphore.OpenExisting("ReadMap"); //读操作 m_Read.WaitOne();//申请一个读操作信号量 //do -> read operation m_Write.Release();//读完成,可以进行写操作 //写操作 m_Write.WaitOne();//申请一个写操作信号量 //d0 -> write operation m_Read.Release();//写完成,可以进行读操作
二、实现(参考博客:https://www.cnblogs.com/lanshy/p/4441450.html)
1.Kernel32.dll帮助类
public class SharedMemory { public const int INVALID_HANDLE_VALUE = -1; public const int PAGE_READWRITE = 0x04; public const int FILE_MAP_ALL_ACCESS = 0x0002; public const int FILE_MAP_WRITE = 0x0002; //shared memory [DllImport("Kernel32.dll", EntryPoint = "CreateFileMapping")] public static extern IntPtr CreateFileMapping( IntPtr hFile,//物理文件句柄 UInt32 lpAttributes,//安全设置 UInt32 flProtect,//保护设置 UInt32 dwMaximumSizeHigh,//高位文件大小 UInt32 dwMaximumSizeLow,//低位文件大小 string lpName//共享内存名称 ); [DllImport("Kernel32.dll", EntryPoint = "OpenFileMapping")] public static extern IntPtr OpenFileMapping( UInt32 dwDesireAccess,//DWORD dwDesiredAccess, int bInheritHandle,//BOOL bInheritHandle string lpName//LPCTSTR lpName ); [DllImport("Kernel32.dll", EntryPoint = "MapViewOfFile")] public static extern IntPtr MapViewOfFile( IntPtr hFileMappingObject,//HANDLE hFileMappingObject, UInt32 dwDesiredAccess,//DWORD dwDesiredAccess UInt32 dwFileOffsetHight,//DWORD dwFileOffsetHigh, UInt32 dwFileOffsetLow,//DWORD dwFileOffsetLow, UInt32 dwNumberOfBytesToMap//SIZE_T dwNumberOfBytesToMap );//[DllImport("Kernel32.dll", EntryPoint = "UnmapViewOfFile")] //public static extern int UnmapViewOfFile(IntPtr lpBaseAddress); //[DllImport("Kernel32.dll", EntryPoint = "CloseHandle")] //public static extern int CloseHandle(IntPtr hObject); }
2.进程A
创建(或声明)共享内存映射、读写信号量,获取地址,设置长度,并开启监听线程
public class AppOneMain { public static AppOneMain Instance = new AppOneMain(); private Semaphore m_Write;//可写的信号 private Semaphore m_Read;//可读的信号 private IntPtr handle;//文件句柄 private IntPtr addr;//共享内存地址 private uint mapLength;//共享内存长 private Thread threadRead;//读数据线程 public void Start() { Init(); } /// <summary> /// 初始化共享数据内存 /// </summary> private void Init() { m_Write = new Semaphore(1, 1, "WriteMap"); m_Read = new Semaphore(0, 1, "ReadMap"); mapLength = 1024; IntPtr hFile = new IntPtr(SharedMemory.INVALID_HANDLE_VALUE); handle = SharedMemory.CreateFileMapping(hFile, 0, SharedMemory.PAGE_READWRITE, 0, mapLength, "shareMemory"); addr = SharedMemory.MapViewOfFile(handle, SharedMemory.FILE_MAP_ALL_ACCESS, 0, 0, 0); Console.WriteLine("Server is running..."); threadRead = new Thread(new ThreadStart(ThreadReceive)); threadRead.Start(); } /// <summary> /// 接收线程 /// </summary> private void ThreadReceive() { while (true) { try { m_Read.WaitOne(); byte[] byteStr = new byte[100]; byteCopy(byteStr, addr); string str = Encoding.Default.GetString(byteStr, 0, byteStr.Length); Console.WriteLine("接收到的消息:" + str); m_Write.Release(); } catch (WaitHandleCannotBeOpenedException) { continue; } } } static unsafe void byteCopy(byte[] dst, IntPtr src) { fixed (byte* pDst = dst) { byte* pdst = pDst; byte* psrc = (byte*)src; while ((*pdst++ = *psrc++) != ‘\0‘) ; } } }
3.进程B
获取读写信号量用于控制读写,打开共享内容映射并写入。
public partial class Form1 : Form { private Semaphore m_Write; //可写的信号 private Semaphore m_Read; //可读的信号 private IntPtr handle; //文件句柄 private IntPtr addr; //共享内存地址 private uint mapLength; //共享内存长 public Form1() { InitializeComponent(); mapLength = 1024; } private void button1_Click(object sender, EventArgs e) { try { m_Write = Semaphore.OpenExisting("WriteMap"); m_Read = Semaphore.OpenExisting("ReadMap"); handle = SharedMemory.OpenFileMapping(SharedMemory.FILE_MAP_WRITE, 0, "shareMemory"); addr = SharedMemory.MapViewOfFile(handle, SharedMemory.FILE_MAP_ALL_ACCESS, 0, 0, 0); m_Write.WaitOne(); byte[] sendStr = Encoding.Default.GetBytes(textBox2.Text.ToString() + ‘\0‘); //如果要是超长的话,应另外处理,最好是分配足够的内存 if (sendStr.Length < mapLength) Copy(sendStr, addr); m_Read.Release(); } catch (WaitHandleCannotBeOpenedException) { MessageBox.Show("不存在系统信号量!"); return; } } static unsafe void Copy(byte[] byteSrc, IntPtr dst) { fixed (byte* pSrc = byteSrc) { byte* pDst = (byte*)dst; byte* psrc = pSrc; for (int i = 0; i < byteSrc.Length; i++) { *pDst = *psrc; pDst++; psrc++; } } } }