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

内存映射文件「建议收藏」在做科研,实现一些大数据的算法的时候,经常要调用一些文件的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)
上一篇 2022年6月17日 下午7:16
下一篇 2022年6月17日 下午7:36


相关推荐

  • Linux命令练习.ziw

    Linux命令练习.ziw 2017年1月10日,星期二Linux命令练习1、统计/usr/bin/目录下的文件个数;#ls/usr/bin|wc-l判断/home/goldin目录是否有文件2、取出当前系统上所有用户的shell,要求,每种shell只显示一次,并且按顺序进行显示;#cut-d:-f7/etc/passwd|sort-u4、取出/etc/inittab文件的第6行;#…

    2022年10月12日
    5
  • ntp 校时 linux 带源码

    ntp 校时 linux 带源码最近做个项目,想通过公司上的NTP服务器给板子校时,但是板子里没有ntpdate这个命令,下面是2个解决方法,1,找到ntpdate源代码,重新编译之后,手动运行,这个方法我上网上查了,比较复杂,据说NTP还与SSL有关,编译的时候必须把SSL也包含进去,于是就迟迟没有动工。2,突然有一天看到rtthread里也提供一个ntp的客户端,比较简单,就一个文件,也没几行,于是想着把这个.c文件移植到linux下,但我仔细研究了一下,发现这个文件的原始作者就是在linux下设计的。原始文件更简单.

    2022年6月18日
    26
  • IJ快捷键

    IJ快捷键ctrl+shift+alt:多行操作psvm:生成main()方法;fori:生成for循环;Ctrl+Alt+v:自动补齐返回值类型ctrl+o:覆写方法ctrl+i:实现接口中的方法ctrl+shift+u:大小写转换CTRL+SHIFT+Z:取消撤销Alt+Insert:生成构造方法、getter、setterctrl+y:删除当前行Ctrl+Shift+J:将选中的行合并成一行ctrl+g:定位到某一行Ctrl+Shitft+向下箭头:将光标所在的代码块向下整体移动Ct.

    2022年6月27日
    118
  • CAD快捷键大全

    CAD快捷键大全作者简介 大家好 我是泽奀 全栈领域新星创作者 个人主页 weixin 的博客 泽奀 CSDN 博客 点赞 评论 收藏 习惯 常用功能键 F1 获取帮助 F2 实现作图窗和文本窗口的切换 F3 控制是否实现对象自动捕捉 F4 数字化仪控制 F5 等轴测平面切换 F6 控制状态行上坐标的显示方式 F7 栅格显示模式控制 F8 正交模式控制 F9 栅格捕捉模式控制 F10

    2026年3月17日
    2
  • map与java对象相互转换

    map与java对象相互转换最近,研究map与java对象之间的相互转换,总结了5种方法:第一种:使用org.apache.commons.beanutils转换用到的主要jar包:commons-beanutils-1.9.3.jar//map转java对象publicstaticObjectmapToObject(Map&lt;String,Object&gt;map,Class&lt;?&g…

    2022年6月11日
    49
  • qt 实现的 lua 编辑器

    qt 实现的 lua 编辑器还不太熟悉 lua 编程 要求实现一个 lua 编辑器网上找了个 qt 写的 c 编辑器 在此基础上改的 nbsp 基本功能实现了 lua 编程需求该软件为 lua 编辑编译器 编译需要 lua 支持 该软件是基于 QtCreator4 7 编写的要软件正常运行需要 qt 的动

    2026年3月18日
    2

发表回复

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

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