共享内存mmap()和CreateFileMapping()

共享内存是进程间的一种通信机制,而且它的作用不止于此。它的主要作用包括:提高I/O效率,匿名内存映射,内存共享进程通信。两个不同进程共享内存的意思是,同一快物理内存映射到该两个进程的进程地址空间。使用共享内存可以通过mmap()返回的指针直接操作文件中的内容,省去了read,write以及lseek等文件读写的操作,提高了I/O效率。并且共享内存是一中效率较高的进程间的通信机制,因为当通信建立时,进程在对共享内存的操作的时候,并不是读写少量数据时就解除映射,等有新的通信的时候,在重新建立共享内存的映射;而是一直保持共享内存区域,直到通信完成为止,这样数据一直保存在共享内存区域,并没有写回文件。通常是在解除映射的时候,才会会写回文件。

那么怎样保持多个进程映射到同一个共享内存的内存页面呢?

我们知道虚拟内存的技术,简单的来说就是在真实的物理内存之外再在硬盘空间增加一个交换区(swap),也就是进程地址空间用到的是虚拟内存,多个进程的虚拟内存的总和是大于物理内存的,那么就安装一定的分配方案把暂时不用的虚拟内存对应的物理内存中的数据保存到硬盘中去(也就是swap area),然后将腾出来的物理内存供给有需要的进程使用。虚拟内存和物理内存有一种映射关系,而且他们是以页为单位映射的,并非是逐个字节的映射。当进程调用mmap()时,只是在进程地址空间中产生了一块缓冲区域,并没有建立其到物理内存的映射关系。因此第一访问该空间时,会引发一个缺页异常。

当我们对普通文件进行操作的时候,通常是把它们读入内存中才能对其执行read,write,lseek等操作。当对其建立虚拟内存到物理内存的映射关系时,系统是先查找该文件是否在page cache(当前的物理内存区域中),如果没有找到,就是还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应的地址,同时,进程页表也会更新。

如果是对与共享内存映射的情况,缺页处理程序会现在page cache中寻找目标页,找到后直接返回相应的地址;如果没有找到,则进入swap cache中寻找目标页,如果找到则执行一个换入操作,并返回相应的地址;如果没有找到,则分配新的物理页面,并把它读入page cache中,更新进程页表。

普通文件的映射和共享内存映射有一个主要的不同点就是后者会查找swap cahe区域中是否存在目标页,这是因为为了让不同进程映射到同一个内存页面,考虑到不同进程的运行中可能把该内存页面换入到swap区域,因此要在swap区域中寻找该目标页,如果创建一个新的物理页面,这样会使不同进程映射到了不同的内存页面。


下面就着重的介绍Linux和Windows下的共享内存如何使用。

Linux下有关共享内存的常用函数有:mmap() ,munmap()以及msync()。

1. mmap()创建共享内存映射

原型:

#include 
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); 
参数:

返回参数:void指针,它可以为任何类型的指针;是目标页的对应的地址,通过该指针可以对目标页进行操作

addr:指定为文件描述符fd应被映射到的进程空间的起始地址。它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。一般默认为NULL。

length:是映射到调用进程地址空间中的字节数,她从被映射文件开头offset个字节处开始算。

prot:负责保护内存映射区的保护。常用值是代表读写访问的PROT_READ | PROT_WRITE.当然还包括数据的执行(PROT_EXEC)、数据不可访问(PROT_NONE)

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

该参数还有更多的值,可以参考mmap的手册。

fd:是有效的文件操作符,一般由open()函数返回,也可以指定为-1,映射匿名共享内存,而且需要flags参数中包含MAP_ANONYMOUS

offset:被映射对象内存的起点,一般设置为0;

2. munmap() 解除共享内存映射

#include 
int munmap(void *addr, size_t length)
参数:

addr:是由mmap()返回的指针;

length:映射区域的大小

3. msync() 

int mysnc(void *addr, size_t length, int flags)
其作用是为了实现磁盘上文件内容和内存中的内容一致。因为mmap()创建的共享内存区域并不是马上的写回磁盘文件中去的,而是等待调用munmap()或msync()后才会写回文件。

下面是使用mmap函数的例子:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main(int argc, char * argv[])
{
	int fd;
	char * buf;
	struct stat sb;
	size_t len;
	char buf_test[40];
	char * filename ="/home/susershine/Documents/temp_file";
	fd = open(filename,O_RDWR|O_CREAT,S_IRUSR | S_IWUSR );
	if(fd == -1)
	{
		perror("open failed!\n");
		return 1;
	}
	if(fstat(fd, &sb) == -1)
	{
		perror("fstat failed\n");
		return 1;
	}
	write(fd, "I am a freshman for linux!", 26*sizeof(char));
	lseek(fd, 0 , SEEK_SET);
	read(fd, buf_test, 26);
	buf_test[26] = '\0';
	printf("original data is : %s\n", buf_test);
	buf = mmap(0, 10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	if(buf == MAP_FAILED)
	{
		perror("mmap failed!\n");
		return 1;
	}
	if(close(fd) == -1)
	{
		perror("close failed!\n");
		return 1;
	}

	for(len = 0; len < 26; len ++)
	{
		buf[len] = toupper(buf[len]);
	}
	printf("now the data is :");
	for(len = 0; len < 26; len++)
	{
		printf("%c", buf[len]);
	}

	printf("\n");
	
	if ((munmap(buf, 10*sizeof(char))) == -1)
	{
		perror("munmap failed");
		return 1;
	}
	return 0;
}
上面函数的运行结果如下:

从结果中可以看出,利用mmap返回的指针buf就可以对文件中的内容进行操作,其实这里没必要用write和read的,可以利用buf指针实现对文件的读写操作。

继续举一个父子间进程通信的例子,由于是具有亲缘关系的进程间的通信,因此可以设置fd为-1,创建一个匿名的共享内存区域。代码如下

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 #include 
  7 int main()
  8 {
  9         int i , sum, fd;
 10         sum = 0;
 11         int *result = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,fd, 0);
 12         *result = 10;
 13         pid_t pid = fork();
 14         if(pid == 0)
 15         {
 16                 for(i = 0; i < 10; i++ )
 17                 {
 18                         sum += i;
 19                 }
 20                 *result = sum;
 21                 printf("this is the son!\n");
 22         //      _exit(0);
 23         }
 24         else
 25         {
 26                 wait(NULL);
 27                 printf("the result is %d\n", *result);
 28                 munmap(result, 4);
 29         }
 30         return 0;
 31 
 32 }
运行结果:


利用共享内存实现了,父子间进程的通信。这里要注意的是:如果将创建子进程的fork函数该为vfork函数,那么要把子进程中被注释的_exit()解除注释,否则会产生Segment fault。

今天查了查VC中的共享内存的实现,发现自己实在是太笨了,这里就只给出函数的原型,至于具体参数怎么解释,可以到msdn上查找有关的函数定义。http://msdn.microsoft.com/en-us/library/aa366761(v=vs.85).aspx 以后遇到VC中的函数不懂的就可以去MSDN上去查找。

HANDLE WINAPI CreateFileMapping(
  _In_      HANDLE hFile,
  _In_opt_  LPSECURITY_ATTRIBUTES lpAttributes,
  _In_      DWORD flProtect,
  _In_      DWORD dwMaximumSizeHigh,
  _In_      DWORD dwMaximumSizeLow,
  _In_opt_  LPCTSTR lpName
);
这个函数返回的是一个句柄,里面参数的具体解释可以参考msdn,注意第一个参数可以使用 INVALID_HANDLE_VALUE。然后是MapViewOfFile函数

LPVOID WINAPI MapViewOfFile(
  _In_  HANDLE hFileMappingObject,
  _In_  DWORD dwDesiredAccess,
  _In_  DWORD dwFileOffsetHigh,
  _In_  DWORD dwFileOffsetLow,
  _In_  SIZE_T dwNumberOfBytesToMap
);
它的作用是创建一个共享内存,和mmap类似,返回的是共享内存的对应指针。在CreateFileMapping 函数中已经设置了共享内存的名字,因此在另一个进程中可以使用OpenFileMapping 函数打开该共享内存,并且返回一个句柄。

HANDLE WINAPI OpenFileMapping(
  _In_  DWORD dwDesiredAccess,
  _In_  BOOL bInheritHandle,
  _In_  LPCTSTR lpName
);
利用该句柄,继续调用MapViewOfFile函数就可以在另一个进程中设置该共享内存的对应指针了。这只是实现共享内存的简约的布局,具体的可以参考msdn中有关的设置。当我们不在使用共享内存时,要释放其所占的资源,应该调用UnmapViewOfFile函数

BOOL WINAPI UnmapViewOfFile(
  _In_  LPCVOID lpBaseAddress
);
当然对我们所获的句柄我们也需要调用,CloseHandle函数。

下面就借用MSDN上有关CreateFileMapping的范例进行解释具体如何使用共享内存。

/*
   This program demonstrates file mapping, especially how to align a
   view with the system file allocation granularity.
*/

#include 
#include 
#include 

#define BUFFSIZE 1024 // size of the memory to examine at any one time

#define FILE_MAP_START 138240 // starting point within the file of
                              // the data to examine (135K)

/* The test file. The code below creates the file and populates it,
   so there is no need to supply it in advance. */

TCHAR * lpcTheFile = TEXT("fmtest.txt"); // the file to be manipulated

int main(void)
{
  HANDLE hMapFile;      // handle for the file's memory-mapped region
  HANDLE hFile;         // the file handle
  BOOL bFlag;           // a result holder
  DWORD dBytesWritten;  // number of bytes written
  DWORD dwFileSize;     // temporary storage for file sizes
  DWORD dwFileMapSize;  // size of the file mapping
  DWORD dwMapViewSize;  // the size of the view
  DWORD dwFileMapStart; // where to start the file map view
  DWORD dwSysGran;      // system allocation granularity
  SYSTEM_INFO SysInfo;  // system information; used to get granularity
  LPVOID lpMapAddress;  // pointer to the base address of the
                        // memory-mapped region
  char * pData;         // pointer to the data
  int i;                // loop counter
  int iData;            // on success contains the first int of data
  int iViewDelta;       // the offset into the view where the data
                        //shows up

  // Create the test file. Open it "Create Always" to overwrite any
  // existing file. The data is re-created below
  hFile = CreateFile(lpcTheFile,
                     GENERIC_READ | GENERIC_WRITE,
                     0,
                     NULL,
                     CREATE_ALWAYS,
                     FILE_ATTRIBUTE_NORMAL,
                     NULL);

  if (hFile == INVALID_HANDLE_VALUE)
  {
    _tprintf(TEXT("hFile is NULL\n"));
    _tprintf(TEXT("Target file is %s\n"),
             lpcTheFile);
    return 4;
  }

  // Get the system allocation granularity.
  GetSystemInfo(&SysInfo);
  dwSysGran = SysInfo.dwAllocationGranularity;

  // Now calculate a few variables. Calculate the file offsets as
  // 64-bit values, and then get the low-order 32 bits for the
  // function calls.

  // To calculate where to start the file mapping, round down the
  // offset of the data into the file to the nearest multiple of the
  // system allocation granularity.
  dwFileMapStart = (FILE_MAP_START / dwSysGran) * dwSysGran;
  _tprintf (TEXT("The file map view starts at %ld bytes into the file.\n"),
          dwFileMapStart);

  // Calculate the size of the file mapping view.
  dwMapViewSize = (FILE_MAP_START % dwSysGran) + BUFFSIZE;
  _tprintf (TEXT("The file map view is %ld bytes large.\n"),
            dwMapViewSize);

  // How large will the file mapping object be?
  dwFileMapSize = FILE_MAP_START + BUFFSIZE;
  _tprintf (TEXT("The file mapping object is %ld bytes large.\n"),
          dwFileMapSize);

  // The data of interest isn't at the beginning of the
  // view, so determine how far into the view to set the pointer.
  iViewDelta = FILE_MAP_START - dwFileMapStart;
  _tprintf (TEXT("The data is %d bytes into the view.\n"),
            iViewDelta);

  // Now write a file with data suitable for experimentation. This
  // provides unique int (4-byte) offsets in the file for easy visual
  // inspection. Note that this code does not check for storage
  // medium overflow or other errors, which production code should
  // do. Because an int is 4 bytes, the value at the pointer to the
  // data should be one quarter of the desired offset into the file

  for (i=0; i<(int)dwSysGran; i++)
  {
    WriteFile (hFile, &i, sizeof (i), &dBytesWritten, NULL);
  }

  // Verify that the correct file size was written.
  dwFileSize = GetFileSize(hFile,  NULL);
  _tprintf(TEXT("hFile size: %10d\n"), dwFileSize);

  // Create a file mapping object for the file
  // Note that it is a good idea to ensure the file size is not zero
  hMapFile = CreateFileMapping( hFile,          // current file handle
                NULL,           // default security
                PAGE_READWRITE, // read/write permission
                0,              // size of mapping object, high
                dwFileMapSize,  // size of mapping object, low
                NULL);          // name of mapping object

  if (hMapFile == NULL)
  {
    _tprintf(TEXT("hMapFile is NULL: last error: %d\n"), GetLastError() );
    return (2);
  }

  // Map the view and test the results.

  lpMapAddress = MapViewOfFile(hMapFile,            // handle to
                                                    // mapping object
                               FILE_MAP_ALL_ACCESS, // read/write
                               0,                   // high-order 32
                                                    // bits of file
                                                    // offset
                               dwFileMapStart,      // low-order 32
                                                    // bits of file
                                                    // offset
                               dwMapViewSize);      // number of bytes
                                                    // to map
  if (lpMapAddress == NULL)
  {
    _tprintf(TEXT("lpMapAddress is NULL: last error: %d\n"), GetLastError());
    return 3;
  }

  // Calculate the pointer to the data.
  pData = (char *) lpMapAddress + iViewDelta;

  // Extract the data, an int. Cast the pointer pData from a "pointer
  // to char" to a "pointer to int" to get the whole thing
  iData = *(int *)pData;

  _tprintf (TEXT("The value at the pointer is %d,\nwhich %s one quarter of the desired file offset.\n"),
            iData,
            iData*4 == FILE_MAP_START ? TEXT("is") : TEXT("is not"));

  // Close the file mapping object and the open file

  bFlag = UnmapViewOfFile(lpMapAddress);
  bFlag = CloseHandle(hMapFile); // close the file mapping object

  if(!bFlag)
  {
    _tprintf(TEXT("\nError %ld occurred closing the mapping object!"),
             GetLastError());
  }

  bFlag = CloseHandle(hFile);   // close the file itself

  if(!bFlag)
  {
    _tprintf(TEXT("\nError %ld occurred closing the file!"),
           GetLastError());
  }
  system("pause");
  return 0;
}
自己运行下就知道上面代码的运行结果了。里面有关共享内存的创建和使用的代码部分是比较明了的,但是为什么要引入一些叫做dwFileMapSize,dwFileSize,dwMapViewStart 和FILE_MAP_START这一系列的变量名呢?在msdn上提到了这么一个概念:allocation granularity,分配粒度。那么什么是分配粒度呢?借用别人博客上说的:Windows 将保留的进程地址空间中的每个区域对齐成一个“整”(二进制的整数)的边界。这个边界由系统的分配粒度(allocation  granularity)值定义。通过Windows的 GetSystemInfo 函数,可获得此分配粒度值。 (下面是windows核心编程中的一段话)每当你保留地址空间的一个区域时,系统要确保该区域从一个分配粒度的边界开始。对于不同的C P U平台来说,分配粒度是各不相同的。但是,截止到撰写本书时,所有的C P U平台(x 8 6、3 2位A l p h a、6 4位A l p h a和I A - 6 4)都使用6 4 K B这个相同的分配粒度。
当你保留地址空间的一个区域时,系统还要确保该区域的大小是系统的页面大小的倍数。页面是系统在管理内存时使用的一个内存单位。与分配粒度一样,不同的C P U,其页面大小也是不同的。x 8 6使用的页面大小是4 KB,而A l p h a(当既能运行3 2位Windows 2000也能运行6 4位Windows 2000时)使用的页面大小则是8 KB。在撰写本书时, M i c r o s o f t预计I A - 6 4也使用8K B的页面。但是,如果测试显示使用更大的页面能够提高系统的总体性能,那么M i c r o s o f t可以切换到更大的页面(1 6 K B或更大)。

那么我们到这里应该明白了,之前定义的那么多变量是为了设置正确的映射MAP的地址。我们以上面的代码做解释:首先通过GetSystemInfo获得当前分配粒度值为64kb;在代码开始处我们定义了FILE_MAP_START为138240(135k),这个变量的意思是指映射内存在文件中开始的位置是从135k开始的;bufersize为1024(1k)是指需要对1k的数据进行访问。即满足共享内存可以访问文件中从135k开始的1k大小的数据。也就是我们需要创建一个大小为136k的共享内存,在CreateMapOfFile中也是这么做的。但是我们系统的分配粒度是64k,那么我们映射的内存的首地址必须是64k的整数倍,且不能大于136k,那么只能是128k,也就是代码中的dwFileMapStart,在MapViewOfFile中也确实用到了这个参数。总体来理解就是:我们先创建一个136k大小的文件,然后按128k的偏移量产生一个映射MAP。当我们利用MapViewOfFile返回的指针访问操作原文件中135k开始出的1k的数据时,我们要使该返回指针偏移FILE_MAP_START - dwFileMapOfFile的大小,才是我们应该访问的数据的地址。代码中

  // Calculate the pointer to the data.
  pData = (char *) lpMapAddress + iViewDelta;
语句就是这个作用。







你可能感兴趣的:(linux进程通信)