Windows之内存映射文件

来源:互联网 发布:好穿的家居拖鞋知乎 编辑:程序博客网 时间:2024/05/17 03:57

=====================Windows之内存映射文件=====================

几乎每个应用程序都要处理文件,但要处理好并不容易。应用程序到底是先应该打开文件、再读取文件,最后关闭文件呢,还是应该先打开文件,再用一个缓存算法来读取和写入

文件的不同部分?Windows为我们提供了一个两全其美的解决方案---内存映射文件。

内存映射文件允许开发人员预订一块地址空间区域并给区域调拨物理存储器。不同之处在于,内存映射文件的物理存储器来自磁盘上已有的文件,而不是来自系统的页交换文件。一旦把文件映射到地址空间,我们就可以对它进行访问,就好像整个文件都已经被载入内存一样。

 

内存映射文件的三种情况:

l       系统使用内存映射文件来载入并运行.exe和动态链接库dll文件。这大量节省了页交换文件的空间以及应用程序启动的时间。

 

l       开发人员可以用内存映射文件来访问磁盘上的数据文件。这可以避免直接对文件进行I/O操作和对文件内容进行缓存。

 

l       通过内存映射文件,可以在同一台机器的不同进程之间共享数据。Windows提供的一些进程间传送数据的方法都是通过内存映射文件来实现的。

 

======================================================================

 

映射到内存的可执行文件.exe和动态链接库DLL

 

当一个线程调用CreateProcess的时候,系统执行以下步骤。

(1)确定所制定的可执行文件所在的位置。如果无法找到该.exe文件,那么系统将不会创建进程,返回NULL

(2)系统创建一个新的进程内核对象。

(3)系统为新进程创建一个私有地址空间。

(4)系统预订一块足够打的空间来容纳.exe。默认情况下.exe文件的基地址是0x0040 0000。只需在构建.exe文件时使用/BASE连接器开关就可以指定一个不同的基地址。

(5)系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的.exe文件,而并非来自系统的页交换文件。

(6).exe文件映射到地址空间之后,会访问.exe文件中的一个段,这个短列出了一些DLL文件,他们包含.exe文件调用到的函数。然后系统会调用LoadLibrary来载入每个DLL。默认情况下DLL的基地址设置为0x1000 0000。同样可以在构建DLL时使用/BASE连接器开关就可以指定一个不同的基地址。

(7)系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件,而并非来自系统的页交换文件。

(8)如果因为某些原因系统无法将.exe文件和所需的DLL文件映射到地址空间区域,系统会先给用户显示一个对话框,然后释放进程的地址空间和进程对象。这时CreateProcess会返回FALSE,我们可以通过调用GetLastError来查询为什么无法创建进程。

(9)当所有的.exeDLL文件都被映射到进程的地址空间之后,系统开始执行.exe文件的启动代码。系统会负责所有的换页,缓存以及高速缓存操作。如果.exe文件中的代码跳转到一个指定地址,但该地址尚未载入内存,那么会引发一个页面错误。系统会检测到这个错误并且自动将该页代码从文件影响载入到内存中。然后系统会把该内存页映像到进程地址空间中的适当位置,并让线程继续执行,就好象该页代码早已载入内存一样。当然,这一切对应用程序来说都是透明的。

第二个实例的地址空间

代码页面1

代码页面2

代码页面3

数据页面1

数据页面2

第一个实例的地址空间

代码页面1

代码页面2

代码页面3

数据页面1

数据页面2

虚拟内存

代码页面2

代码页面1

数据页面2

代码页面3

数据页面1

新页面

注意:

同一个可执行文件或DLL的多个实例不会共享静态数据。如果当实例1试图修改数据页面2中的一个全局变量时,系统会分配一个新的虚拟内存页,然后把数据页面2中的内容复制到新页面中。然后系统会更新第一个实例的地址空间,新的数据页面就回和原始数据页面一样,映射到进程地址空间中的同一位置。现在系统不仅可以让进程修改全局变量的值,也不用担心会修改到同一个应用程序的其它实例的数据了。

 

.exeDLL文件映像的组成:

常用段属性

.bss : 未经初始化的数据段

.data : 已初始化的数据段

.reloc : 重定位表信息

.tls : 线程本地存储

.rdata : 只读的运行时数据

.debug : 调式信息

.text : .exe文件和DLL的代码

我们可以用编译器指示符来创建自己的段。

#pragma data_seg(“sectionname”)

LONG g_lInstanceCount = 0;       //必须已经初始化

#pragma data_seg()

 


======================================================================

 

映射到内存的数据文件

为什么要使用内存映射文件呢?我们来看一个颠倒文件内容的例子。我们可以用通常的办法,先把文件载入到内存中,当然既要考虑到内存空间的利用率又要考虑到磁盘空间的使用情况,而且还要考虑数据在写回过程中不会丢失,这看起来并不吓人但将会是比较困难的。

而如果使用内存映射文件,只需要打开文件并向系统预订一块虚拟地址空间。接着系统把文件的第一个字节映射到该区域中的第一个字节。然后便可以访问这个虚拟内存区域,就好象它实际上包含了文件一样。在这种情况下,直接调用C运行库函数_tscrev就能颠倒文件中的数据。这么做的好处是我们不必再分配任何内存,把文件中的数据载入内存,吧数据写回文件、以及释放内存块。但遗憾的是如果操作过程中断电仍然能导致数据被破坏。

如何使用内存映射文件呢?接下来将会看到创建的过程。

1.         创建或打开文件内核对象

HANDLE CreateFile(

PCSTR pszFileName,                             //要打开的文件名

DWORD dwDesiredAccess,                   //文件的访问权限

DWORD dwShareMode,                        //共享文件方式

PSECURITY_ATTRIBUTES psa,            //安全属性设置

DWORD dwCreateDisposition,               //创建设备部署

DWORD dwFlagsAndAttributes,             //文件属性标志

HANDLE hTemplateFile                          //模板文件句柄

);

调用CreateFile是为了告诉操作系统文件映射的物理存储器所在的位置。路径可以是文件在磁盘、网络或者光盘上所在的位置。

 

2.         创建文件映射内核对象

HANDLE CreateFileMapping(

HANDLE hFile,                                     //文件句柄,来自第一步生成的文件句柄

PSECURITY_ATTRIBUTES psa,            //安全属性设置

DWORD fdwProtect,                             //指定保护属性

DWORD dwMaximumSizeHigh,              //内存文件映射的最大大小字节数的高32

DWORD dwMaximumSizeLow,              //32

PCTSTR pszName                                 //文件映射对象的名称

);

调用CreateFileMapping的作用是告诉系统文件映射对象需要多大的物理存储器。

 

3.         将文件的数据映射到进程的地址空间

HANDLE MapViewOfFile(

HANDLE hFileMappingObject,                //文件映射对象句柄

DWORD dwDesiredAccess,                   //文件的访问权限

DWORD dwFileOffsetHigh,                    //映射到视图中的第一个数据字节高32

DWORD dwFileOffsetLow,                    //32

SIZE_T dwNumberOfBytesToMap          //数据文件的多少字节被映射到地址空间

);

这部分主要告诉系统两件事。

第一,   我们必须告诉系统应该把数据文件中的哪个字符映射到视图中的第一个字符。

第二,   我们必须告诉系统要把数据文件中的多少映射到地址空间中去。

函数的返回PBYTE pbFile指针,以后便可以直接对这个指针进行操作就像是对文件操作一样。

 

4.         从进程的地址空间撤销对文件数据的映射

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

pvBaseAddress就是上一步中的pbFile。如果不这样做,在进程终止之前,区域都得不到释放。

如果需要将数据立即写回到磁盘中,可以调用以下函数

BOOL FlushViewOfFile(

       PVOID pvAddress,

       SIZE_T dwNumberOfBytesToFlush);

 

 

5.         关闭文件映像对象和文件对象

CloseHandle(hFileMapping);

CloseHandle(hFile);

如果不关闭句柄会引起资源泄漏。让我们做一个“合格”的程序员。


======================================================================

 

内存映射文件处理大文件

 

当处理大容量文件时,普通32位地址空间的操作无法满足,这时就需要使用内存映射文件了,一开始吧文件开头的部分映射到内存中。完成对文件的第一个视图的访问后,可以撤销对文件中一部分的映射,然后把另一部分映射到视图中。一直重复这个过程,知道完成对整个文件的访问。

原创粉丝点击