来自Windows核心编程 – 第十七章 第三节 – 使用内存映射文件。

本小节会讲到如何使用内存映射文件,在最后会附上一个实例教程。

要使用内存映射文件,需要执行下面三个步骤:

  1. 创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射文件的那个磁盘文件。
  2. 创建一个文件映射内核对象来告诉系统文件的大小以及我们打算如何访问文件。
  3. 高速系统把文件映射对象的部分或全部映射到进程的地址空间中。

用完内存映射文件后,必须执行下面三个步骤来做清理工作:

  1. 告诉系统从进程地址空间中取消对文件映射内核对象的映射
  2. 关闭文件映射内核对象
  3. 关闭文件内核对象

 

17.3.1 第一步:创建或打开文件内核对象

我们总是调用 CreateFile 函数来创建或打开一个文件内核对象:

这里总共有七个参数,其中有几个不会详细讲解:

  1. pszFileName 表示想要创建或打开的文件的名称(可以包含路径,也可以不包含)
  2. dwDesiredAccess 用来指定打算如何访问文件的内容。稍后会有一个表列出。
  3. dwShareMode 告诉系统我们打算如何共享这个文件。同样会列表。
  4. psa 是安全对象。所有的内核对象都有这个参数。
  5. dwCreationDisposition 参数对文件的含义重大,会有列表给出。
  6. dwFlagsAndAttributes 参数有两个用途,一个是允许我们设置一些标志来微调与设备之间的通信,另一个是如果设备是一个文件,我们还能够设置文件的属性。后面我们会介绍通信标志和文件属性。
  7.  hTemplateFile 既可以标识一个已经打开的文件句柄,也可以是NULL.

以上是几个参数的作用,接下来会对他们进行详细的讲解。

第一个参数:略…这有啥好讲的…文件名。

第二个参数是如何访问文件的内容,一般是以下四个值之一:

 dwDesiredAccess 值  含义
 0 既不能读取文件也内容,也不能写入数据。
如果想取得文件的属性,可以传入0,即NULL
 GENERIC_READ 可以读取文件
 GENERIC_WRITE 可以写入文件
 GENERIC_READ | GENERIC_WRITE 既可以读取,也可以写入。

要注意的是,对内存映射文件来说,必须以只读或者读/写方式来打开文件。

第三个参数告诉系统我们打算如何共享这个文件:

 dwShareMode 值  含义
 0 其他任何试图打开文件的操作都会失败,不共享。
 FILE_SHARE_READ 其他任何试图通过 GENERIC_WRITE 来打开文件的操作都会失败
只读共享
 FILE_SHARE_WRITE 其他任何试图通过 GENERIC_READ 来打开文件的操作都会失败
唯写共享
 FILE_SHARE_READ | FILE_SHARE_WRITE 其他任何试图打开文件的操作都会成功,读写共享。

第四个参数:参考内核对象这一章:http://blog.tk-xiong.com/archives/518

第五个参数:列表如下:

 dwCreationDisposition 值  含 义
 CREATE_NEW  创建一个新文件,如果同名文件已经存在,则会调用失败
 CREATE_ALWAYS  告诉CreateFile 无论同名文件存在与否都会创建一个新文件。
如果已存在则会覆盖原来的文件
 OPEN_EXISTING  打开一个已有的文件,如果文件不存在,那么会调用失败
 OPEN_ALWAYS  打开一个已有的文件,如果文件存在会直接打开
如果文件不存在,会创建一个新的文件
 TRUNCATE_EXISTING  打开一个已有的文件并将文件大小截断为0,如果文件不存在,则调用失败。

第六个参数:dwFlagsAndAttributes 参数的作用是设置一些标志来微调设备间的通信,如果设备是一个文件,我们还能够设置文件的一些属性。

通信标志都是一些信号,告诉系统我们打算以何种方式来访问设备。这样系统就可以对缓存算法进行优化。

下面我们会下你介绍通信标志,然后再介绍文件属性。

1. CreateFile 的高速缓存标志 —— 主要关注文件系统对象

FILE_FLAG_NO_BUFFERING 这个标志表示在访问文件的时候不要使用任何数据缓存。为了提高性能,系统在访问磁盘的时候会对数据进行缓存。我们通常不指定这个标志,于是告诉缓存管理器就能够将文件系统中最近访问的那部分保存在内存中。这样如果我们先从文件中读取几个字节,然后再读取几个字节,那么文件的数据可能在我们第二次读取之前就已经载入到了内存中,这样我们就只需要访问一次硬盘了,这样显著提高了性能。

但是要注意的是,速度的提升是从文件中读取超过实际需要的数据量来达到的。如果我们不再从文件中读取数据,可能会浪费内存。通过指定 FILE_FLAG_NO_BUFFERING 标志,告诉告诉告诉缓存器我们不希望它对任何数据进行缓存——我们自己对数据进行缓存。这个标志可以提高应用程序的性能和内存的使用效率。由于文件系统会将文件数据直接写入到我们提供的缓存中,因此我们必须遵循一定的规则:

  • 在访问文件的时候,使用的偏移量必须正好是磁盘卷的扇区大小的整数倍。
  • 读取/写入文件的字节数必须正好是扇区大小的整数倍。
  • 必须确保缓存在进程地址空间中的起始地址正好是扇区大小的整数倍。

但是有一种情况下必须用 FILE_FLAG_NO_BUFFERING 这个参数。首先我们要知道的是,为了对一个文件进行管理,高速缓存器必须为该文件保存一些内部数据结构,文件越大,所需要的结构也就越多。在处理非常大的时候,高速缓存器可能无法分配文件所需的内部数据结构,从而导致文件打开失败。所以为了访问非常大的文件,我们必须设置这个标志来打开文件。

 

FILE_FLAG_SEQUENTIAL_SCAN 标志 和 FILE_FLAG_RANDOM_ACCESS 标志 只有当我们允许对文件数据进行缓存的时候,这些标志才有用。

如果指定了 FILE_FLAG_SEQUENTIAL_SCAN 标志,那么系统会认为我们将顺序地访问文件。当我们从文件中读取数据时,系统从文件中实际读取的数据量会超过我们所要求的数量。这个过程减少了对硬盘的访问速度并提高了应用程序的运行速度。但是如果我们重置文件指针,那么系统所花费的额外时间以及在内存中的数据就都狼给了。

如果我们经常需要重置文件指针的话,可以指定 FILE_RANDOM_ACCESS 标志,这个标志告诉系统不要提前读取文件数据。

 

FILE_FLAG_WRITE_THROUGH 这是最后一个与高速缓存有关的标志。它禁止对文件写入操作进行缓存以减少丢失数据的可能性。当我们指定这个标志的时候,系统会对所有文件的修改直接写入到磁盘中。但是系统仍然在内部的缓存中保存文件数据,这样文件读取操作会继续使用缓存中的数据,而不必直接从磁盘读取数据。如果用这个标志来打开网络服务器上的文件,那么只有在数据已经被写入到服务器磁盘后,各个Windows文件写入函数才会返回到调用线程。

 

2. CreateFile 的其他标志

FILE_FLAG_DELETE_ON_CLOSE 使用这个标志可以让文件系统在文件所有句柄都被关闭后,删除该文件。这个标志通常和 FILE_ATTRIBUTE_TEMPORARY 属性一起使用。当这两个标志一起使用的时候,系统可以创建一个临时文件,向文件中写入 数据,从文件中读取数据,最后关闭文件。当关闭文件的时候,系统会自动删除该文件。

 

FILE_FLAG_BACKUP_SEMANTICS 这个标志一般用于备份和恢复文件。在打开或创建任何文件之前,为了确保试图打开文件 或 创建文件的进程具有所需访问的特权,系统通常会执行安全性检查。但是备份和恢复软件有一定的特殊性,它们会跳过某些文件安全性检查。当我们指定了这个标志的时候,系统会检查调用者的存取令牌是否具备对文件和目录进行备份/恢复特权。如果调用者具备相应的特权,那么系统会允许它打开文件。我们也可以用这个标志来打开一个目录的句柄。

 

FILE_FLAG_POSIX_SEMANTICS 在Windows中,文件名会保留最初命名时使用的大小写,而在查找文件的时候文件名是不区分大小写的。但是POSIX子系统要求在查找文件的时候区分大小写。这个标志让CreateFile 在创建文件或打开文件的时候,以区分大小写的方式来查找文件名。在使用 FILE_FLAG_POSIX_SEMANTICS 的时候要极其小心,如果在创建一个文件的时候用这个标志,那么Windows应用程序可能无法访问该文件。

 

FILE_FLAG_OPEN_REPARSE_POINT 它告诉系统忽略文件的重解析属性。

重解析属性允许一个文件系统过滤器对打开文件、读取文件、写入文件以及关闭文件这些行为修改。

 

FILE_FLAG_OPEN_NO_RECALL 这个标志告诉系统不要将文件内容从脱机存储器(比如磁盘)恢复到关联存储器(比如硬盘)。

 

FILE_FLAG_OVERLAPPED 这个标志告诉我们以异步方式来访问。

 

3. 文件属性标志

 文件属性标志  含义
FILE_ATTRIBUTE_ARCHIVE 文件是一个存档文件。应用程序用这个标志来讲文件标记为待备份或待删除,一般创建一个新文件的时候会自动设置这个标志。
FILE_ATTRIBUTE_ENCRYPTED 文件是经过加密的。
FILE_ATTRIBUTE_HIDDEN 文件是隐藏的,它不会出现在通常的目录清单中。
FILE_ATTRIBUTE_NORMAL 文件没用其他属性。只有单独使用的时候,这个标志才有效
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED 内容索引服务不会对文件进行索引。
FILE_ATTRIBUTE_OFFLINE 文件虽然存在,单文件内容已经被转移到脱机存储器中。
FILE_ATTRIBUTE_READONLY 文件是只读的。可以读取文件,但不能写入或删除文件。
FILE_ATTRIBUTE_SYSTEM 文件是操作系统的一部分,或专供操作系统使用。
FILE_ATTRIBUTE_TEMPORARY 文件数据只会使用一小段时间。问了将访问时间降至最低,会尽量将文件数据保存在内存中而不是保存在磁盘中。

如果我们要创建临时文件的话,那么应该使用 FILE_ATTRIBUTE_TEMPORARY 标志。当我们把这个标志和前面介绍到的 FILE_FLAG_DELETE_ON_CLOSE 标志组合使用的时候,可以提高系统的性能。当我们关闭的时候,会直接删除它。

第七个参数:刚才讲了那么久…我们还在讨论CreateFile的参数还记得吗…

接下来是hTemplateFile,它既可以标识一个已经打开的文件的句柄,也可以是NULL。

如果hTemplateFile标识一个文件句柄,那么CreateFile会完全忽略 dwFlagsAndAttributes 参数,并转而使用 hTemplateFile 所标识的文件的属性。为了能够让函数以这种方式工作,hTemplateFile标识的文件必须是一个已经用GENERIC_READ 标志打开的文件。

如果CreateFile要打开已有文件而不是创建新文件,那么它会忽略hTemplateFile参数。

如果CreateFile成功地创建或打开了文件或设备,那么它会返回文件或设备句柄。

如果CreateFile失败了,那么它会返回 INVALID_HANDLE_VALUE 

 

大多数以句柄为返回值的Windows函数在失败的时候会返回NULL,但是在CreateFile返回的却是 INVALID_HANDLE_VALUE (-1) 。所以如果要判断是否创建成功的正确代码如下:

 

17.3.2 第二步:创建文件映射内核对象

调用CreateFile时为了告诉操作系统文件映射的物理存储器所在的位置。传入路径是文件在磁盘上所在的位置,文件映射对象的物理存储器来自该文件。现在我们就要告诉系统文件映射内存对象需要多大的物理存储器。

这个步骤需要调用 CreateFileMapping 函数:

六个参数,讲述其中四个…

第一个参数 hFile:

是需要映射到进程地址空间的文件的句柄。该句柄是前面调用CreateFile的时候返回的。

第二个参数是安全对象。传递NULL就好了

第三个参数是一个保护属性。一般还是下面五个属性:

保护属性 含义
PAGE_READONLY 完成对文件映射对象的映射时,可以读取文件中的数据。在调用CreateFile时必须传入 GENERIC_READ
PAGE_READWRITE 可以读/写文件数据。调用CreateFile时必须传入 GENERIC_READ | GENERIC_WRITE
PAGE_WRITECOPY 可以读写,并且写入操作会创建一个副本。调用CreateFile时必须传入GENERIC_READ 或者 GENERIC_READ|GENERIC_WRITE
PAGE_EXECUTE_READ 可以读可运行,在调用CreateFile时必须传入 GENERIC_READ 和 GENERIC_EXECUTE
PAGE_EXECUTE_READWRITE 可读可写运行,在调用CreateFile时必须传入 GENERIC_READ、GENERIC_WRITE 和 GENERIC_EXECUTE.

除了前面提到的页面保护属性,我们还可以把五种段属性与 CreateFileMapping 的 fdwProtect 参数按位或起来。这个“段”只不过是内存映射的另一种叫法。

这些段属性的第一个是 SEC_NOCACHE ,它告诉系统不要对内存映射文件的页面进行缓存。因此如果把数据写入文件,那么与通常的情况相比,系统会更加频繁地更新磁盘上的文件。这个标志和 PAGE_NOCACHE 保护属性相似,主要是给驱动程序开发人员使用的。普通应用程序一般不会用到。

第二个段属性是 SEC_IMAGE,它告诉系统要映射的文件是一个PE文件映像。当系统把文件映射到进程地址空间的时候,系统会检查文件的内容并决定应该给各页面指定何种保护属性。

第三个段属性 和 第四个段属性分别是 SEC_RESERVE 和 SEC_COMMIT 这里不讲。

最后一个段属性是 SEC_LARGE_PAGES,它告诉Windows要为内存映射文件使用大页面内存。只有当用于PE映像文件或内存映射文件的时候,这个属性才是有效的。

接下里两个参数是很重要的,分别是 dwMaximumSizeHigh 和 dwMaximumSizeLow。

因为CreateFileMapping函数的主要目的是为了确保有足够的物理存储器可供文件映射对象使用。这两个参数告诉系统内存映射文件的最大大小。以字节为单位。由于Windows支持最大文件大小可以用64位整数表示,因此这里必须使用过两个32位值。其中 High表示的是高32位…Low表示的是低32位。对小于4GB的文件来说,dwMaximumSizeHigh这个参数永远都是0.

如果想要用当前的文件大小创建一个文件映射对象,那么只要传0给这两个参数就可以了。如果想要读取文件或者在不改变文件大小的前提下访问文件,那么同样需要传0给这两个参数。

如果想要给文件追加数据,那么在选择文件最大大小的时候应该留有余地了。

要注意的是,如果当前文件大小是0,那么我们就不能给这两个参数传递0.因为这样做就相当于告诉系统我们想要一个大小为0的内存映射对象。CreateFileMapping函数会认为这样是错误的,并返回NULL.

最后一个参数是 pszName,是一个以0为终止符的字符串,用来给文件映射内存对象指定一个名称。这个名称用来在不同的进程间共享文件映射对象。通常不需要共享的话,传递NULL即可。

如果无法创建文件映射对象的话,我们会返回NULL…

 

17.3.3 第三步:将文件的数据映射到进程的地址空间

前一步我们创建了文件映射对象,接下来我们要为文件的数据预定一块地址空间并将文件的数据作为物理存储器调拨给区域。这一步通过 MapViewOfFile 来实现:

五个参数…

第一个参数 hFileMappingObject 是文件映射对象的句柄,他是之前调用 CreateFileMapping 或 OpenFileMapping 函数返回的。

第二个参数 dwDesiredAccess 表示想要如何访问文件数据…是的,我们必须再次指定我们打算如何访问文件的数据。可以指定下表中的5个值之一:

保护属性 含义
 FILE_MAP_WRITE 可以读取和写入文件,在调用CreateFileMapping的时候必须传入PAGE_READWRITE属性。
 FILE_MAP_READ 可以读取文件。在调用CreateFileMapping的时候可以传入 PAGE_READONLY 或 PAGE_READWRITE 保护属性。
 FILE_MAP_ALL_ACCESS 等同于 FILE_MAP_WRITE |  FILE_MAP_READ | FILE_MAP_COPY
 FILE_MAP_COPY 可以读取和写入文件。写入操作会导致系统为该页面创建一份副本。在调用 CreateFileMapping 时必须传入 PAGE_WRITECOPY 保护属性。
 FILE_MAP_EXECTUE 可以将文件中的数据作为代码来执行。在调用CreateFileMapping 时可以传递 PAGE_EXECUTE_READWRITE 或者 PAGE_EXECUTE_READ 保护属性。

剩下的三个参数与预定地址空间区域和给区域调拨物理存储器有关。当我们把一个文件映射到进程的地址空间的时候,不必一下子映射整个文件。可以每次只把文件的一小部分映射到地址空间中。文件中被映射到进程地址空间中的部分被称为视图,其名称 MapViewOfFile(文件的映射视图) 便源于此了。

把文件的一个视图映射到进程的地址空间的时候,必须告诉系统两件事:

第一,必须告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节。这是通过参数dwFileOffsetHigh 和 dwFileOffsetLow来指定的。要注意的是文件的偏移量必须是系统分配粒度的整数倍(Win – 64KB)

第二,我们必须告诉系统要把数据文件中的多少映射到地址空间中去。这和预订地址空间时需要指定区域的大小一样。参数 dwNumberOfBytesToMap 用来指定大小。如果指定的大小为0,那么系统会试图把文件中从偏移量开始到文件末尾的所有部分都映射到视图中。

注意这一点:无论整个文件映射对象有多大,MapViewOfFile 只需要找到一块足够大的地址空间区域来容纳指定的视图。

如果在调用MapViewOfFile 的时候 指定了 FILE_MAP_COPY 标志,那么系统会从页交换文件中调拨物理存储器,调拨的物理存储器的大小由 dwNumberOfBytesToMap 参数决定。对文件映射视图进程操作时,只要我们不执行读取数据之外的任何操作,系统就不会用到从页交换文件中调拨的页面。但是一旦哪个线程写入文件映射视图中的任何内存地址,系统就会从页交换文件中已调拨的页面中选择一个页面,把原始数据复制到页交换文件中的页面,然后把复制的页面映射到进程的地址空间中。此后,各线程都会访问数据的副本,而不是访问或修改原始的数据了。

系统对原始数据进行复制是,会把页面的保护属性从 PAGE_WRITECOPY 改成 PAGE_READWRITE 。

 

17.3.4 第四步:从进程的地址空间撤销对文件数据的映射

不再需要把文件的数据映射到进程的地址空间中时,可以调用下面的函数来释放内存区域:

这个函数唯一的参数 pvBaseAddress 用来指定区域的基地址,它必须和 MapViewOfFile函数 的返回值相同。

出于对速度的考虑,系统会对文件数据的页面进行缓存处理,这样在处理文件映射视图的时候就不需要随时更新磁盘上的文件。如果需要确保所做的修改已经被写入到磁盘中,那么可以调用 FlushViewOfFile ,这个函数用来强制系统把部分或者全部修改过的数据写回到磁盘中:

两个参数,一个是内存映射文件视图中第一个字节的地址。函数会把传入的地址向下取整到页面大小的整数倍。

第二个参数表示要刷新的字节数,会把这个参数向上取整到页面大小的整数倍。

如果没有修改任何数据的话,会直接返回。

UnmapViewOfFile 有一个特征是需要牢记的。如果试图最初是用 FILE_MAP_COPY 标志映射的,那么对文件数据的修改实际上是对保存在也交换文件中的文件数据的副本的修改。如果在这里种情况下调用 UnmapViewOfFile 函数的话,不会对磁盘文件进行任何更新,但它会释放页交换文件中的页面,从而导致数据丢失。

如果希望保留修改过的数据的话,就必须进行额外的操作了。

例如:可以为同一个文件 用PAGE_READWRITE 创建另一个文件映射对象,并用 FILE_MAP_WRITE 标志把这个新的 文件映射对象 映射到进程的地址空间中。然后可以在第一个视图中查找具有 PAGE_READWRITE 保护属性的页面,只要找到一个具有该保护属性的页面,就可以对其内容进行检查,并决定是否需要将修改过的数据写入文件。如果不写入的话继续查找就好了,直到文件尾截止。如果想保存的话,把数据页面从第一个视图复制到第二个视图即可。由于第二个视图的保护属性是 PAGE_READWRITE,所以会更新文件位于磁盘上的实际内容。

 

17.3.5 第五步和第六步:关闭文件映射对象 和 文件对象

调用两次 CloseHandle 函数,每次关闭一个句柄。

我们必须关闭自己打开的任何内核对象,不然会再进程继续运行的过程中引起资源泄露。

 

整个过程大致如下:

当然还有另外一种方法,不过这里就不列举了。提一下:引用计数的使用。

 

17.3.6 这里以 File Reverse 示例程序为例来进行内存映射文件的使用。

以上代码在 VS2013 – Win10 是亲测可用的。

如有疑问请留言。

【Windows核心编程】使用内存映射文件
Tagged on:
0 0 投票数
Article Rating
订阅评论
提醒

0 评论
内联反馈
查看所有评论