内存映射文件「建议收藏」

在做科研,实现一些大数据的算法的时候,经常要调用一些文件的I/O函数,在数据量很大的时候,除了设计的算法和数据结构的耗时以外,其实主要的耗时还是文件的I/O。因为一般常规的方法就是先读出磁盘文件的内容到内存中,然后修改,最后写回到磁盘上。读磁盘文件是要经过一次系统调用,先将文件的内容从磁盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,实际上是两次数据拷贝。写回同样也需要经过两次数据拷贝。所以整个过程基本上会有至少四次数据的拷贝,文件稍微大一点,I/O的开销还是很大的。因此有必要采取一定的措施来减少这方面的耗时。内存映射文件是操作系统的提供的一种机制,操作系统将一个数据文件的地址映射到进程的地址空间中,即内存映射数据文件,在对大量的数据进行操作时,这样做会非常方便。

原理解读

内存映射文件memeory-mapped file)是操作系统本身内存管理的一种机制,它的思想就是保留一定的地址空间区域,将物理存储器提交到这个区域。这里的物理存储器与虚拟内存不同,是来自一个已经位于磁盘上的文件,而不是系统的页面文件。一旦映射这个文件就可以访问这个文件,如同把这个文件加载到内存。这个想法是微软提供的,也有相关的接口函数,这种想法并不是简单的I/O操作,涉及到windows操作系统的核心技术——内存管理方面的知识。

使用内存映射文件一方面可以用来加载和执行. exe和DLL文件,大大节省页文件空间和应用程序启动运行所需的时间,另一方面,可以用来访问磁盘上的数据文件,不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。此外,使用内存映射文件,可以使同一台机器上运行的多个线程之间共享数据。Windows也提供了其他在进程之间通信的一些方法,以便在进程之间进行数据通信,但这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单台机器上的多个进程之间进行通信的最有效的方法。

使用步骤

使用内存映射文件的步骤如下:

1)    创建或打开一个文件内核对象,用这个对象来标识磁盘上需要用作内存映射文件的文件。

2)    创建一个文件映射内核对象,告诉系统该文件的大小以及这个文件的访问方式。

3)    让系统将文件映射对象的全部或一部分映射到进程的地址空间中。

当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:

1)    告诉系统从进程的地址空间中撤消文件映射内核对象的映像。

2)    关闭文件映射内核对象。

3)    关闭文件内核对象。

步骤1:创建或打开文件内核对象,可以调用CreateFile函数:

HANDLE CreateFile
 LPCTSTR lpFileName,    // 指向文件名的指针 
 DWORD dwDesiredAccess,    // 访问模式(写 / 读) 
 DWORD dwShareMode,    // 共享模式 
 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 指向安全属性的指针 
 DWORD dwCreationDisposition,   // 如何创建 
 DWORD dwFlagsAndAttributes,   // 文件属性 
 HANDLE hTemplateFile    // 用于复制文件句柄 
);

调用CreateFile函数,就可以告诉操作系统文件映像的物理存储器的位置,传递的路径名用于指明支持文件映像的物理存储器在磁盘(或网络或光盘)上的确切位置。这时,还必须告

诉操作系统,文件映射对象需要多少物理存储器,需要调用下面的函数来完成这个操作。

步骤2:创建一个文件映射内核对象,调用CreateFileMapping函数。

HANDLE CreateFileMapping
   HANDLE hFile,                       //物理文件句柄
  LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
  DWORD flProtect,                    //保护设置
  DWORD dwMaximumSizeHigh,            //高位文件大小
  DWORD dwMaximumSizeLow,             //低位文件大小
  LPCTSTR pszName                      //共享内存名称
);

第一个参数hFile用来标识需要映射到进程地址空间中的文件句柄,这个句柄由前面调用的CreateFile函数返回。

在创建一个文件映射对象之后,系统依然需要为文件的数据保留一个地址空间区域,并将文件的数据作为映射到这个区域的物理存储器进行提交,调用下面的函数可以完成这个操作。

步骤3:将文件数据映射到进程的地址空间,可以调用MapViewOfFile函数:

LPVOID MapViewOfFile
   HANDLE hFileMappingObject,  // 已创建的文件映射对象句柄
  DWORD dwDesiredAccess,      // 访问模式
  DWORD dwFileOffsetHigh,     // 文件偏移的高32位
  DWORD dwFileOffsetLow,      // 文件偏移的低32位
  DWORD dwNumberOfBytesToMap  // 映射视图的大小
);

第一个参数hFileMappingObject用来标识文件映射对象的句柄,这个句柄是前面调用CreateFileMapping或者OpenFileMapping时返回的。

将一个文件映射到进程的地址空间中时,不必一次性地映射整个文件,可以只将文件的一小部分映射到地址空间,被映射到进程的地址空间的这部分文件称为一个视图。当将一个文件视图映射到进程的地址空间中时,必须做两件事情。首先,必须告诉系统数据文件中的哪个字节应该作为视图中的第一个字节来映射,可以使用dwFileOffsetHigh和dwFileOffsetLow这两个参数来完成。其次,必须告诉系统,数据文件有多少字节要映射到地址空间,可以使用dwNumberOfBytesToMap这个参数进行设定。

步骤4:从进程的地址空间中撤消文件数据的映像,可以调用UnmapViewOfFile函数将其释放:

BOOL UnmapViewOfFileLPCVOID lpBaseAddress);

lpBaseAddress指定了返回区域的基地址,必须将这个值设定与MapViewOfFile)的返回值相同。如果没有调用这个函数,那么在进程终止运行前,保留的区域就不会被释放。每当调用MapViewOfFile函数时,系统就会在进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。

步骤5和6:关闭文件映射对象和文件对象,为了防止资源泄露的问题,由CreateFile)和CreateFileMapping)函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle)将其释放。

 

具体的编程中,大致的调用过程如下:

HANDLE hFile=CreateFile...);
HANDLE hFileMapping=CreateFileMappinghFile,...);
CloseHandlehFile);
PVOID  pvFile=MapViewOfFilehFileMapping,...);
CloseHandlehFileMapping);
//使用内存映射文件
...
...
...
UnmapViewOfFilepvFile);

如果用同一个文件来创建更多的文件映射对象,或者映射同一个文件映射对象的多个视图,

那么就不能较早地调用CloseHandle函数,因为以后可能还需要使用它们的句柄,以便分别对CreateFileMapping和MapViewOfFile函数进行更多的调用。

使用内存映射处理大文件,比如说把16TB的文件映射到一个较小的内存映射空间,直接全部映射是无法实现的,必须映射一个只包含一小部分文件数据的文件视图。可以这样考虑,首先映射一个文件开始的视图,在完成对文件的第一个视图的访问后,可以取消对这一部分的映射,然后映射文件中更后面的位置开始的视图。重复进行这个操作,直到访问完整个文件为止。

此外,内存映射文件具有一致性。系统允许将一个文件的相同数据映射到多个视图,比如说将一个文件开头的10KB映射到一个视图,然后将这个文件开头的4KB映射到另一个进程。只要映射相同的文件映射对象,系统就会确保映射的视图数据的一致性。比如说,如果应用程序改变了一个视图中的文件内容,其他视图中的数据也会相应地改变。

例子

参考了书籍《windows核心编程》,例子是书中例子的变体。

在一个8GB的二进制文件和32位的地址空间中,计算该二进制文件中所有0字节的数目。

#include <Windows.h>
#include <iostream>
#include <time.h>
using namespace std;
__int64 count){
	//获取系统分配粒度
	SYSTEM_INFO SysInfo;
	GetSystemInfo&SysInfo);

	HANDLE hFile=CreateFile
		TEXT"D:\\data.dat"),
		GENERIC_READ|GENERIC_WRITE,
		FILE_SHARE_READ,
		0,
		OPEN_EXISTING,
		FILE_FLAG_SEQUENTIAL_SCAN,
		NULL
		);
	if hFile==INVALID_HANDLE_VALUE)
	{
		cout<<"创建文件对象失败,错误代码:"<<GetLastError)<<endl;
		return 0;
	}
	HANDLE hFileMapping=CreateFileMapping
		hFile,
		NULL,
		PAGE_READONLY,
		0,
		0,
		NULL);
	if hFileMapping==NULL)
	{
		cout<<"创建文件映射对象失败,错误代码:"<<GetLastError)<<endl;
		return 0;
	}
	DWORD dwFileSizeHigh;
	__int64 qwFileSize=GetFileSizehFile,&dwFileSizeHigh);
	qwFileSize+=__int64)dwFileSizeHigh)<<32);
	CloseHandlehFile);

	__int64 qwFileOffset=0,qwNumOf0s=0;
	while qwFileSize>0)
	{
		//映射到视图的字节数
		DWORD dwBytesInBlock=SysInfo.dwAllocationGranularity;
		if qwFileSize<SysInfo.dwAllocationGranularity)
			dwBytesInBlock=DWORD)qwFileSize;

		PBYTE pbFile=PBYTE)MapViewOfFilehFileMapping,FILE_MAP_READ,
			DWORD)qwFileOffset)>>32,//starting bytes
			DWORD)qwFileOffset&0xFFFFFFFF),//in file
			dwBytesInBlock//# of bytes to map
			);
		
		for DWORD dwByte=0;dwByte<dwBytesInBlock;dwByte++)
		{
			if pbFile[dwByte]==0)
			{
				qwNumOf0s++;
			}
		}
		UnmapViewOfFilepbFile);
		qwFileOffset+=dwBytesInBlock;
		qwFileSize-=dwBytesInBlock;
	}
	CloseHandlehFileMapping);
	return qwNumOf0s;
}
int main)
{
	clock_t start,end;
	start=clock);
	__int64 num=count);
	end=clock);
	cout<<"8GB二进制文件统计总耗时:"<<end-start)/CLOCKS_PER_SEC<<"s"<<endl;
	cout<<"0的个数为:"<<num<<"个"<<endl;
	system"pause");
	return 0;
}

小结

Windows操作系统本身提供了很多优秀的机制,使得应用程序能够快速而方便地共享数据和信息,比如说RPC、COM、OLE、DDE、窗口消息、剪贴板、邮槽、管道、套接字等。在windows中,单台机器上共享数据的最底层机制就是内存映射文件。关于内存映射文件,不只在C++中有,在java的新的IO类型中,也有相关的类,比如说MappedByteBuffer等

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注