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

内存映射文件「建议收藏」在做科研,实现一些大数据的算法的时候,经常要调用一些文件的I/O函数,在数据量很大的时候,除了设计的算法和数据结构的耗时以外,其实主要的耗时还是文件的I/O。因为一般常规的方法就是先读出磁盘文件的内容到内存中,然后修改,最后写回到磁盘上。读磁盘文件是要经过一次系统调用,先将文件的内容从磁盘拷贝到内核空间的一个缓冲区,然后再将这些数据拷贝到用户空间,实际上是两次数据拷贝。写回同样也需要经过两次数据拷

大家好,又见面了,我是你们的朋友全栈君。

在做科研,实现一些大数据的算法的时候,经常要调用一些文件的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 UnmapViewOfFile(LPCVOID lpBaseAddress);

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

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

 

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

HANDLE hFile=CreateFile(...);
HANDLE hFileMapping=CreateFileMapping(hFile,...);
CloseHandle(hFile);
PVOID  pvFile=MapViewOfFile(hFileMapping,...);
CloseHandle(hFileMapping);
//使用内存映射文件
...
...
...
UnmapViewOfFile(pvFile);

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

那么就不能较早地调用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=GetFileSize(hFile,&dwFileSizeHigh);
	qwFileSize+=(((__int64)dwFileSizeHigh)<<32);
	CloseHandle(hFile);

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

		PBYTE pbFile=(PBYTE)MapViewOfFile(hFileMapping,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++;
			}
		}
		UnmapViewOfFile(pbFile);
		qwFileOffset+=dwBytesInBlock;
		qwFileSize-=dwBytesInBlock;
	}
	CloseHandle(hFileMapping);
	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等

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/149900.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • Spring AOP中动态代理的两种实现方式及其过程_ajax工作原理及优缺点

    Spring AOP中动态代理的两种实现方式及其过程_ajax工作原理及优缺点AOP思想:基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强!

    2022年9月17日
    3
  • 【OpenCV】Canny 边缘检测

    【OpenCV】Canny 边缘检测Canny边缘检测算法1986年,JOHNCANNY提出一个很好的边缘检测算法,被称为Canny编边缘检测器[1]。Canny边缘检测根据对信噪比与定位乘积进行测度,得到最优化逼近算子,也就是Canny算子。类似与LoG边缘检测方法,也属于先平滑后求导数的方法。使用Canny边缘检测器,图象边缘检测必须满足两个条件:能有效地抑制噪声;必须尽量精确确定边缘的位置。算

    2022年5月29日
    36
  • PAT乙级题目答案汇总 PAT (Basic Level) Practice (中文)[通俗易懂]

    PAT乙级题目答案汇总 PAT (Basic Level) Practice (中文)[通俗易懂]题目列表:标号题目链接分数博客链接完成时间1001害死人不偿命的(3n+1)猜想151001害死人不偿命的(3n+1)猜想(15分)2020/8/01

    2022年5月29日
    38
  • 国密SM4分组加密[通俗易懂]

    国密SM4分组加密[通俗易懂]分享一篇SM4加密算法实现文章,算法用C语言即可实现,只有短短300多行代码。SMS4是我国无线局域网标准WAPI中所采用的分组密码标准,随后被我国商用密码标准采用,又名SM4(SM是“商密”的缩写,目前公布的其他商密标准包括SM2椭圆曲线公钥密码,SM3密码杂凑算法)。作为我国商用密码的分组密码标准,预计SMS4在国内的敏感但非机密的应用领域会逐渐取代3DES,AES等国外分组密码标准,用于通…

    2022年9月27日
    3
  • 集赞神器!朋友圈集赞一键秒搞定!从此集赞随心所欲!

    集赞神器!朋友圈集赞一键秒搞定!从此集赞随心所欲!今天,刚开始不知道要分享什么内容,下午烦恼时,结果收到一好友“朋友圈帮忙点赞”的消息,瞬间拉黑删除的心都有了,但是呢又不能这样做,点赞也不是,不点赞也不是,强(自)大(恋)的我告诉自己冷静一下,换个角度想问题,灵感来了~不如,今天就分享一下朋友圈一键集赞的方法~从此集赞随心所欲!要是下次再有好友让你帮忙集赞的时候,你可以将本文章甩给他,相信他会感谢你的~千万不要甩给商家!说到朋…

    2022年6月9日
    157
  • android:layout_gravity 和 android:gravity 的区别

    android:layout_gravity 和 android:gravity 的区别

    2021年8月26日
    53

发表回复

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

关注全栈程序员社区公众号