[笔记]Windows核心编程《十七》内存映射文件

系列文章目录

[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]快乐的LInux命令行《五》什么是shell
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理

相关:
参考1
参考2

文章目录

  • 系列文章目录
  • 前言
  • 一、映射到内存的可执行文件和DLL
    • CreateProcess调用过程
    • 通过LoadLibrary从exe和dll的段地址 读取DLL列表
    • 同一个可执行文件或DLL的多个实例不会共享静态数据
      • 如果应用程序的一个实例修改了数据页面中的一些全局变量,那么应用程序所有实例的内存都会被修改。由于这种类型的修改可能会导致灾难性的结果,因此必须避免。
    • 在同一个可执行文件或DLL的多个实例间共享静态数据
      • 背景知识
      • 多个实例共享数据的方法
    • Application Instance示例程序
  • 二、映射到内存的数据文件
    • 方法1:一个文件,一块缓存
    • 方法2:两个文件,一块缓存
    • 方法3:一个文件,两块缓存。
    • 方法4:一个文件,零个缓存。
  • 三、使用内存映射文件
    • 第1步:创建或打开 文件内核对象
    • 第2步 创建文件映射内存对象
    • 第3步 将文件的数据映射到进程地址空间
    • 第4步 从进程的地址空间撤销对文件数据的映射
    • 第5步和第6步 关闭文件映射对象
    • File Reverse 示例程序
  • 四、用内存映射文件来处理大文件
  • 五、内存映射文件和一致性
  • 六、给内存映射文件的指定基地址
  • 七、内存映射文件的实现细节
  • 八、用内存映射文件再进程间共享数据
  • 九、以页交换文件为后背存储器的内存映射文件
  • 十、 稀疏调拨的内存映射文件
  • 十一、文件映射进行进程间通信
  • 总结
    • 1.操作系统中,程序启动过程?加载dll错误处理?
    • 共享内存和文件映射区别?

前言

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

页交换文件,简单讲就是系统用于做虚拟内存的一个磁盘文件
页交换文件相关可参考windows内存体系结构

共享内存是一种特殊的文件映射

内存映射主要用于以下三种情况:

  • 系统使用内存映射文件来载入并运行.exe和动态链接库(DLL)。这大量节省了页交换文件的空间以及应用程序启动的时间。
  • 开发人员可以使用内存映射文件来访问磁盘上的数据文件。这使得我们可以避免直接对文件进行I/O操作和对文件内容进行缓存。
  • 通过使用内存映射文件,我们可以在同一台机器的不同进程间共享数据。Windows的确提供了其它一些方法来在进程间传送数据,但这些方法都是通过内存映射文件来实现的。因此,如果在同一台机器的不同进程之间共享数据,内存映射文件时最高效的方法。

一、映射到内存的可执行文件和DLL

CreateProcess调用过程

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

  1. 系统会先确定CreateProcess所指定的可执行文件所在的位置。如果无法找到该.exe文件,那么系统将不会创建进程,这时CreateProcess会返回FALSE.
  2. 系统创建一个新的进程内核对象。
  3. 系统为新进程创建一个私有地址空间。
  4. 系统预定一块足够大的地址空间来容纳.exe。待预定的地址空间区域的具体位置已经在.exe文件中指定。 默认情况下,.exe文件的基地址是0x00400000(OEP PE可执行程序入口点地址)。但是,只需在构建应用程序的.exe文件时使用/BASE连接器开关,我们就可以给自己的应用程序指定一个不同的地址。
  5. 系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的.exe文件,而并非来自系统的页交换文件。

通过LoadLibrary从exe和dll的段地址 读取DLL列表

当系统把.exe文件映射到进程的地址空间之后,会访问.exe文件中一个段,这个段列出了一些DLL文件,它们包含该.exe文件调用到的函数。然后系统会调用LoadLibrary来载入每个DLL,如果哪个DLL需要用到其它DLL,那么系统同样会调用其它DLL,那么系统同样会调用 LoadLibrary来载入相应的DLL。

每当调用LoadLibrary来加载一个DLL时,系统将执行下列操作步骤,它们均类似上面的第4和第5个步骤:

  • 系统会预定一块足够大的地址空间区域来容纳DLL文件。待预定的的地址空间区域的具体位置已经在DLL文件中指定。

    按照默认设置, Microsoft的Visual C++建立的 D L L文件基地址是0 x10000000(这个地址可能不同于在 64位Windows 2000上运行的6 4位D L L的地址)但是,你可以在创建DLL文件时重载这个地址,方法是使用链接程序的 /BASE选项。所有与Windows一起发布的系统DLL都有不同的基地址,这样即使把它们载入到同一个地址空间,也不会发生重叠。

  • 如果系统无法在DLL文件指定的基地址处预定区域,这可能是因为该区域已经被另一个DLL或.exe占用,也可能是区域不够大,这时系统会尝试在另一个地址 来为DLL预定地址空间区域。

    • 如果DLL不包含重定位信息(当使用连接器的/FIXED开关来构建DLL),这意味着DLL必须被载入到指定的基地址,否则无法被载入。
    • 如果对DLL执行重定位,重定位不仅需要占用页交换文件中额外的存储空间,而且会增加载入DLL所需的时间。
  • 系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件,而并非来自页交换文件。如果由于Windows不能将DLL载入到指定的基地址而必须重定位的话,那么系统还会另外进行标注,表明DLL中有一部分物理存储器映射到了页交换文件。

注意:如果由于某个原因系统无法映射 . e x e和所有必要的D L L文件,那么系统就会向用户显示一个消息框,并且释放进程的地址空间和进程对象。CreateProcess函数将向调用者返回 FALSE,调用者可以调用GetLastError函数,以便更好地了解为什么无法创建该进程。

把所有的.exe文件和DLL文件都映射到进程的地址空间之后,系统会开始执行.exe文件的启动代码。当完成对.exe文件的映射后,系统会负责所有的换页(paging)、缓存(buffering)、以及高速缓存(caching)操作。

例如,
如果.exe文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个错误
系统能够发现这个错误,并且自动将这页代码从该文件的映像加载到一个 RAM页面。
然后,系统将这个RAM页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。
当然,这一切是应用程序看不见的。当进程中的线程每次试图访问尚未加载到RAM的代码或数据时,该进程就会重复执行

同一个可执行文件或DLL的多个实例不会共享静态数据

如果一个应用程序已经在运行,那么当我们为这个应用程序创建一个新的进程时,系统只不过是打开另一个内存映射试图(memory-mapped view),创建一个新的进程对象,并为主线程创建一个新的线程对象。这个新打开的内存映射视图隶属一个文件映射对象(file-mapping object),后者用来标识可执行文件的映像。系统同时给进程对象和线程对象分别制定新的进程ID和线程ID。通过使用内存映射文件,同一个应用程序的多个实例可以共享内存中的代码和数据。

如下图,它描述了如何把可执行程序的代码和数据载入到虚拟内存,并映射到地址空间中。可执行页面---->载入到虚拟内存—>映射到应用程序地址空间
[笔记]Windows核心编程《十七》内存映射文件_第1张图片
假设应用第二个实例程序,这时系统不过是把包含应用程序的代码和数据映射到第二个实例的地址空间中,如下图所示:
[笔记]Windows核心编程《十七》内存映射文件_第2张图片

注意:
实际上,文件的内容被分为段,代码在一个段中,而全局变量再另一个段中,段是对齐到页面大小的整数倍,应用程序可以通过调用GetSystemInfo来检测页面大小,在.exe或DLL文件中,代码段通常在数据段的前面。

如果应用程序的一个实例修改了数据页面中的一些全局变量,那么应用程序所有实例的内存都会被修改。由于这种类型的修改可能会导致灾难性的结果,因此必须避免。

操作系统通过内存管理系统的写时复制特性来防止这种情况的发生。当写入内存映射文件时,系统会截获此尝试,接着为应用程序分配一块新内存,然后复制 页面内容,最终的结果是其它实例不会受到影响。

下图描绘了当应用程序的第一个实例试图修改数据页面2虽的一个全局变量时,会产生的结果。
[笔记]Windows核心编程《十七》内存映射文件_第3张图片

在同一个可执行文件或DLL的多个实例间共享静态数据

背景知识

每个exe文件和dll文件映象都有许多段组成。
比如:

  • 编译器会将代码放在一个叫.text段中,
  • 将已经初始化的数据放在.data段中。
  • 将未初始化得数据放在.bss段中。
    [笔记]Windows核心编程《十七》内存映射文件_第4张图片

每个段都有一些与之关联的属性:
[笔记]Windows核心编程《十七》内存映射文件_第5张图片

多个实例共享数据的方法

自定义自己的段

#pragma data_seg("sectionname")

例如我们可以用下面代码来合建一个名为Share的段,它只包含一个LONG的变量

#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_set();

注意:
编译器只会将已经初始化的变量放入自己定义的段当中,如果上面代码中g_lInstanceCount 没有初始化,则不会放到我们指定的段之中。
但是Vc++ 编译器提供了一个allocate声明符,它允许我们将未经初始化的数据放到任何我们想要放入的段中。

多个实例共享数据的方法:
之所以将变量放在一个单独的段中,最常见的原因就是为了共享exe或dll多个实例中共享数据。

注意:
但是MricroSoft并不鼓励使用共享段:
1.有潜在安全漏洞
2.意味着一个应用程序中的错误可能影响到另一应用程序。

Application Instance示例程序

二、映射到内存的数据文件

Windows系统可以使我们能够把数据文件映射到进程地址空间中,这样一来,对大型数据流进行操作就非常容易。

以颠倒文件内容为例子。四种方法实现。

方法1:一个文件,一块缓存

实现方法具体步骤:

  1. 分配足够大的内存存放整个文件
  2. 打开文件,读取文件到内存中,关闭文件。
  3. 交换第一个字节和最后一个字节,交换第二个字节和倒数第二个字节,以此类推。

缺点:

  • 根据文件大小分配内存,如果文件过大,超过2GB,32位应用程序无法调拨那么大的物理存储器,大文件需要其他方法。
  • 将颠倒顺序的文件内存写入文件过程中处理被中断,文件内容会被破坏。避免方法是将原始文件先复制一份副本,整个过程成功完成,再删除文件副本。这样需要额外的磁盘空间。

方法2:两个文件,一块缓存

实现方法具体步骤:

  1. 打开已有文件并在磁盘创建一个长度为0的新文件
  2. 接着分配一块较小的内部缓存,比如8KB。
  3. 将文件指针定位到原始文件末尾减去8KB的地方。
  4. 将8K的内容读取到缓存中,颠倒缓存的内容。
  5. 将颠倒后的内容写入新创建的文件。
  6. 重复步骤,定位文件指针,读取文件,颠倒缓存,写入文件的过程一直继续,直到到达原始文件的起始位置。
    (如果文件的长度不是8KB的整数倍,需要特殊处理)
  7. 完全处理完原始文件后,将两个文件都关闭并删除原始文件。

优缺点:

  • 只分配8KB,内存使用率更高,但是需要文件指针定位操作,所以处理速度慢。
  • 新文件逐渐增大,到1G。比实际需要的磁盘空间多了整整1GB。

方法3:一个文件,两块缓存。

实现方法具体步骤:

  1. 程序初始化时分配两块大小为8KB的缓存。
  2. 把文件开始的8KB内容读取到第一块缓存中,把文件末尾的8KB内容读取到另一块缓存中。
  3. 然后把内容颠倒,第一块缓存写回到文件末尾,第二块缓存写回到文件开头i。
  4. 这样过程一直继续,每次都把靠近文件开头的8KB和靠近文件尾部的8KB内容交换
    (如果文件大小不是,16KB的整数倍,最后两块8KB缓存会有重叠,需要特殊处理)

优缺点:

  • 整个过程所有数据读取写入到同一个文件,不需要额外磁盘空间。
  • 处理过程被打断,可能会导致数据文件损坏。(同方法1)

方法4:一个文件,零个缓存。

实现方法具体步骤:

  1. 使用内存映射文件来颠倒文件内容时,我们先打开文件并向系统预订一块虚拟地址空间区域。
  2. 接着让系统把文件的第一个字节映射到该区域的第一个字节。
  3. 然后就可以访问这块虚拟内存区域,就好像它实际上包含了文件一样。事实上,如果要颠倒的是一个文本文件,而且文件末尾字节为0,则可以把这个文件当作内存中的一个字符串来处理,在这种情况下,直接调用c运行库函数_tcsrev就能颠倒文件中的数据。

优缺点:

  • 这种方法最大的优点就是让系统为我们处理文件缓存有关操作时,无需缓存。
  • 但遗憾的是,使用内存映射文件的时候,如果操作过程被中途打断(如断电),仍然可能导致数据被破坏

三、使用内存映射文件

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

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

使用完后清理工作,需要执行以下三个步骤:

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

略,暂时不看 需要实践代码。https://www.cnblogs.com/zhangdongsheng/p/3269515.html

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

第2步 创建文件映射内存对象

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

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

第5步和第6步 关闭文件映射对象

File Reverse 示例程序

四、用内存映射文件来处理大文件

如何将一个16EB的文件映射到一个较小的地址空间中?
一开始,我们只映射文件开头的部分到视图中,完成对文件的第一个视图的访问后,我们可以撤销对文件这一部分的远射,然后把文件的另一部分映射到视图中。我们一直重复这个过程,直到完成对整个文件的访问。

五、内存映射文件和一致性

结论: 系统允许我们把同一个文件中的数据映射到多个视图中。只要我们映射的是同一个文件映射对象,那么系统会确保各视图中的数据是一致的。

原因: 这是因为即使该页面被多次映射到进程的虚拟地址空间中,系统也还是在同一个内存页面中保存被映射的数据。如果多个进程把同一个数据文件映射到多个视图中,那么数据也仍然会是一致 的,这是因为数据文件中的每个页面在内存中只有一份——但这些内存页面会被映射到多个进程的地址空间中。

说明: Windows允许我们以同一个数据文件为后备存储器来创建多个文件映射对象。Windows并不保证这些不同的文件映射对象的各个视图是一致的。系统只保证在同一文件映射对象的多个视图间保持一致。

简而言之,就是多个程序读取映射文件时,各个程序是读取的是一致的,因为映射文件只有一份,其他程序的地址空间都是指向同一份映射文件。

六、给内存映射文件的指定基地址

在调用VirtualAlloc的时候,我们可以建议系统在指定的基地址预定地址空间。同样,也可以用MapViewOfFileEx函数来代替MapViewOfFile函数,这样就能建议系统把文件映射到指定的地址:

 PVOID MapViewOfFileEx(
	HANDLE hFileMappingObject,
	DWORD dwDesiredAccess,
	DWORD dwFileOffsetHigh,
	DWORD dwFileOffsetLow,
	SIZE_T dwNumberOfBytesToMap,
	PVOID pvBaseAddress
);
除了最后一个参数 pvBaseAddress ,这个函数的所有参数和返回值都与MapViewOfFile函数完全相同。我们可以用这个参数来给要映射的文件制定一个目标地址。同VirtualAlloc函数一样,指定的目标地址必须是分配粒度的整数倍。
使用内存映射文件跨进程共享数据的时候,MapViewOfFileEx非常有用。给共享的数据文件在不同的进程指定相同的基地址。

七、内存映射文件的实现细节

在进程能够从自己的地址空间中访问内存映射文件的数据之前,Windows要求进程先调用MapViewOfFile。不同进程调用MapViewOfFile时返回的内存地址,很可能是不同的。

八、用内存映射文件再进程间共享数据

Windows提供了多种机制,使得应用程序之间能够快速、方便地共享数据和信息。在Windows中,在同一台机器上共享数据的最底层的机制就是内存映射文件。
让 我们来看一个例子:启动应用程序。当一个应用程序启动时,系统会先调用CreateFile来打开磁盘上的.exe文件。接着系统会调用 CreateFileMapping来创建文件映射对象。最后系统会以新创建的进程的名义调用MapViewOfFileEx(并传入SEC_IMAGE 标志),这样就把.exe文件映射到了进程的地址空间中。值所以调用MapViewOfFileEx而不是MapViewOfFile,是为了把文件映射 到指定的基地址,这个基地址保存在.exe的PE文件头中。系统然后创建进程的主线程,在映射得到的视图中取得可执行代码的第一个字节的地址,把该地址放 到线程的指令指针中,最后让CPU开始执行其中的代码。
如 果用户启动同一个应用程序的第二个实例,那么系统会发现该.exe文件已经有一个文件映射对象,因此就不会再创建一个新的文件对象或文件映射对象。取而代 之的是,系统会再次映射.exe文件的一个视图,但这次是在新创建的进程的地址空间中。至此,系统已经把同一个文件同时映射到了两个地址空间中。显然,由 于物理内存中包含.exe文件可执行代码的那些页面为两个进程所共享,因此内存的使用率更高。

九、以页交换文件为后背存储器的内存映射文件

Microsoft加入了相应的支持,让系统能够创建以页交换文件为后备存储器的内存映射文件,这样就不需要用磁盘上专门的文件来作为后备存储器了。这种方法和为磁盘文件创建内存映射文件的方法几乎完全相同,甚至更简单。 一方面,由于不必创建或打开一个专门的磁盘文件,因此不需要调用CreateFile。

我们只需要像原来那样调用CreateFileMapping,并将INVALID_HANDLE_VALUE作为hFile参数传入。这告诉系统我们创建的文件映射对象的物理存储器不是磁盘上的文件,而是希望系统从页交换文件中调拨物理存储器

所需分配的存储器大小由CreateFileMapping的dwMaximumSizeHigh和dwMaximumSizeLow参数决定。

十、 稀疏调拨的内存映射文件

CreateFileMapping为我们提供了一种方法,即在fdwProtect参数中指定SEC_RESERVE或SEC_COMMIT标志。这样我们把先前的那个电子的数据作为一个文件映射对象来共享,但又不希望在一开始就给它调拨所有物理存储器。

SEC_RESERVE:系统不会从页交换文件中调拨物理存储器,它只返回文件映射对象的句柄。现在我们可以调用MapViewOfFile来给这个文件映射对象创建一个视图。MapViewOfFile会预订地址空间,但不会调任何物理存储器。设计图访问会违规。通过使用SEC_RERVER和VirtualAlloc我们不仅能与其它进程共享电子表格的数组,而且还能高效的使用物理存储器。如果内存映射文件是通过SEC_RESERVE标志得到的,便不能用VirtualFree来撤销调拨给它的存储

NT文件系统(NTFS)提供了对稀疏文件的支持。这是一项非常棒的特征。我们可以用这项特性来创建和处理稀疏内存映射文件,这样一来,存储器就不必总是在页交换文件中,而可以在普通文件中。

假设我们要创建一个内存映射文件来存储录音数据。当用户说话时,我们希望把数字音频数据写入到内存缓存中,并以磁盘文件为内存缓存的后备存储器。一个部分调拨的内存映射文件当然是最简单高效的办法。问题是我们不知道用户在单击停止按钮前会说多久,可能是五分钟,但也可以是5小时。差距不可谓不大!我们需要一个足够大的文件来保存这些数据。但是,在使用稀疏调拨的内存映射文件时,大小并没有多大关系。

十一、文件映射进行进程间通信

不同进程对信号量Count进行累加

#include 
#include 
using namespace std;

int main()
{
	HANDLE hMap;
	PINT lpMapAddr;

	// get file mapping's handle 
	hMap = OpenFileMapping(
		FILE_MAP_ALL_ACCESS,	// 权限:所有
		FALSE,					// 不继承句柄
		"TEST_Map"				// 名字
		);
	if (NULL == hMap) {
		// init
		hMap = CreateFileMapping(
			INVALID_HANDLE_VALUE,	// 不是真实的文件,所以写 INVALID_HANDLE_VALUE
			NULL,					// 同上
			PAGE_READWRITE,			// 该句柄对文件映射可读可写
			0,						// 与下一个参数连用,表示文件映射的大小
			1024,					// 与上一个参数连用,表示文件映射的大小
			"TEST_Map"				// 名字
			);
		if (NULL == hMap) {
			goto end;
		}
		lpMapAddr = (PINT)MapViewOfFile(
			hMap,						// 文件映射句柄
			FILE_MAP_ALL_ACCESS,		// 权限:所有
			0,							// 与下一个参数连用,表示文件映射起始地址偏移
			0,							// 与上一个参数连用,表示文件映射起始地址偏移
			0							// 映射整个文件映射对象
			);
		*lpMapAddr = 1;
	}

	// get file mapping's address
	lpMapAddr = (PINT)MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

	// read 
	cout << "instance's count : " << *lpMapAddr << endl;

	// write
	*lpMapAddr = *lpMapAddr + 1;


	cout << "finished." << endl;
	getchar();
end:
	if (hMap)
		CloseHandle(hMap);
	getchar();
	return 0;
}

[笔记]Windows核心编程《十七》内存映射文件_第6张图片

总结

1.操作系统中,程序启动过程?加载dll错误处理?

运行可执行模块时,操作系统的加载程序执行步骤
windows 内存管理总结
[笔记]Windows核心编程《十七》内存映射文件_第7张图片

共享内存和文件映射区别?

共享内存时一种特殊的文件映射,使用的是磁盘页交换文件为内存交换文件

你可能感兴趣的:(#,windows核心编程,windows)