Loading... # Windows核心编程-学习笔记10 ## 内存映射文件 内存映射文件允许预定一个地址空间区域并为区域调拨物理存储,但这个物理存储来自磁盘上已有的文件,而不是系统分页文件 内存映射文件用于: * 加载并执行exe和dll,节省分页文件的空间和应用程序的启动时间 * 访问磁盘上的数据文件,避免文件I/O操作和缓冲文件内容 * 在同一台机器的不同进程之间共享数据 ### 映射到内存的可执行文件和DLL 系统在线程调用CreateProcess时: 1. 系统先定位调用CreateProcess时指定的可执行文件。找不到exe文件,进程就不会创建,返回FALSE 2. 系统新建一个进程内核对象 3. 系统为该进程创建一个私有地址空间 4. 系统预定一个足以容纳exe文件的地址空间区域,该区域首选的位置已在exe文件中指定。默认x86 0x400000,x64 0x140000000 5. 系统会标注预定区域的后备物理存储来自磁盘上的exe文件而不是系统的分页文件 系统将exe文件映射到进程的地址空间之后,会访问exe文件中的一个段,其中列出了一些DLL文件,包含在exe中调用的函数。然后系统针对每个dll都调用LoadLibrary;其中的任何DLL用到了其他DLL,系统同样会调用LoadLibrary来加载DLL。 对于DLL: 1. 系统预定一个足以容纳DLL文件的地址空间区域。默认x86 0x10000000,x64 0x180000000。所有与windows一起发布的标准系统dll都有不同的基地址,这样即使加载到同一个地址空间也不会发生重叠 2. 如果系统无法在DLL文件指定的基地址预定区域,这可能是因为该区域已被另一个DLL或exe占用,也可能区域不够大,这时系统会尝试在其他地址为DLL预定地址空间区域。这样不好有两点,一是DLL去除了重定位信息,系统可能无法加载DLL;二是系统在DLL内部需要重定位,占用分页文件额外的存储,还会增大加载DLL所需的时间 3. 系统会进行标注,表明预定区域的后备物理存储来自磁盘上的DLL文件,而非来自系统的分页文件。如果windows不能将DLL加载到他首选的基地址而必须重定位,系统还会另行标注,表明DLL中有一部分物理存储被映射到了分页文件 完成exe文件映射后,系统会负责所有换页、缓冲和高速缓存操作。例如,如果exe中的代码导致它跳转到一条尚未载入内存的指令的地址,就会引发页面错误。系统检测到错误自动将代码页从文件映像加载到一个RAM中。然后系统将该RAM页映射到进程地址空间中的适当位置,并让线程继续执行,就好像代码页早已自如内存一样,这一切对于应用程序来说都是透明的。 #### 同一个可执行文件或DLL的多个实例不会共享静态数据 如果应用程序的一个实例修改了数据页面中的一些全局变量,应用程序所有实例的内存都会被修改。系统通过内存管理系统的**写时复制**特性来防止这种情况发生,每次当应用程序试图向内存映射文件写入时,系统都会截获此类尝试,接着为应用程序试图写入的内存页分配一个新的内存块,复制页面内容,然后让应用程序向刚才分配的内存块写入。这样应用程序的其他实例不会受任何影响。 #### 在同一个可执行文件或DLL的多个实例间共享静态数据 默认不行,但可以利用这个共享数据设计一个变量来保存正在运行实例的数量,新实例只需检查这个全局变量的值就可以确定当前运行实例数目。 每个exe或DLL文件映像由多个段组成,每个标准段名称都以点号开始。 | 段名 | 目的 | | -------- | ------------------------------------- | | .bss | 未初始化的数据 | | .CRT | 只读的C运行时数据 | | .data | 已初始化的数据 | | .debug | 调试信息 | | .didata | 延迟导入的名字表 | | .edata | 导出的名字表 | | .idata | 导入的名字表 | | .rdata | 只读的运行时变量 | | .reloc | 重定位表信息 | | .rsrc | 资源 | | .text | exe或DLL的代码 | | .textbss | 当启用增量链接选项时,由C++编译器生成 | | .tls | 线程局部存储 | | .xdata | 异常处理表 | 每个段都有与之关联的属性: * READ:可以读取数据 * WRITE:可以写入数据 * EXECUTE:可以执行该段内容 * SHARED:该段内容为多个实例所共享 除了上面编译器和链接器创建的标准段,还可以自己定义段 ~~~c++ #pragma data_seg("sectionname") ~~~ 例如下面创建Shared段,只包含一个LONG变量 ~~~c++ #pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_seg() ~~~ 如果没有初始化g_lInstanceCount就会放到Shared段之外的其他段。但可以借助allocate声明标识符将未初始化的数据放到任何希望的段中 ~~~c++ #pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_seg() __declspec(allocate("Shared")) int d; ~~~ 要想共享变量,还需要告诉链接器共享一个特定段的变量。可以通过在链接器的命令行汇总使用/SECTION开关来实现 ~~~ /SECTION:name,attributes ~~~ 例如(S为SHARED) ~~~ /SECTION:Shared,RWS ~~~ 也可以直接将链接器开关嵌入源代码 ~~~c++ #pragma comment(linker, "/SECTION:Shared,RWS") ~~~ ### 映射到内存的数据文件 Windows操作系统允许将数据文件映射到进程的地址空间,这样操控大数据流时会很方便。例如,实现对文件中所有字节进行反转: 1. **一个文件,一个缓冲区**:分配一块足够大的内存来存放整个文件,然后用交换算法。缺点是文件比较大,无法调拨很大物理存储;其次反转顺序后的内容写回文件可能损坏 2. **两个文件,一个缓冲区**:先打开现有文件,并创建一个长度为0的新文件,接着分配一个小的内部缓冲区,比如8KB,然后文件指针定位到原始文件末尾减去8KB的地方,对这部分反转写入新文件直到达到起始位置。缺点是需要操作指针,处理速度慢;其次消耗大量空间(最后2倍原文件) 3. **一个文件,两个缓冲区**:初始化分配两个8KB缓冲区,读取前8KB和后8KB,都反转然后第一个写回文件尾,最后一个写回文件头,以此类推,注意最后一部分不是8KB倍数需要特殊处理。 4. **一个文件,零个缓冲区**:使用内存映射文件,打开文件并向系统预定一个虚拟地址空间区域 ### 使用内存映射文件 1. 创建并打开一个文件内核对象,该对象标识了想要用作内存映射文件的磁盘文件 CreateFile创建文件内核对象,必须以只读或可读可写方式来打开文件 2. 创建一个文件映射内核对象,向系统说明文件的大小以及准备如何访问文件 调用CreateFileMapping ~~~c++ HANDLE CreateFileMapping(HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName); ~~~ * hFile:需要映射到进程地址空间的文件的句柄,前面CreateFile返回的 * psa:指针,通常传递NULL表示默认 * fdwProtect:指定保护属性 * dwMaximumSizeHigh/dwMaximumSizeLow:CreateFileMapping主要目的是确保有足够物理存储可供文件映射对象使用,这两个参数告诉系统内存映射文件的最大大小。要用当前文件大小创建一个文件映射对象,为这个两个参数传递0即可;如果磁盘文件大小为0字节,就不能传两个0,否则系统认为错误返回NULL * pszName:以0为终止符字符串,用来为文件映射对象指定名称 3. 告诉系统将文件映射对象的部分或全部映射到进程的地址空间中 调用MapViewOfFile为文件的数据预定一个地址空间区域 ~~~c++ PVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap); ~~~ 将文件映射到进程的地址空间时,不必一下子映射整个文件。可以每次只将文件的一小部分映射到地址空间中。文件被映射到进程地址空间的部分称为视图(View) * hFileMappingObject:文件映射对象的句柄,前面调用CreateFileMapping或OpenFileMapping返回的 * dwDesiredAccess:指定数据访问方式 * dwFileOffsetHigh、dwFileOffsetLow:告诉系统应该将数据文件中的哪个字节映射到视图中的第一个字节 * dwNumberOfBytesToMap:告诉系统要将数据文件中的多少映射到地址空间 用完后,清理步骤如下: 1. 告诉系统从进程地址空间中取消对文件映射内核对象的映射 调用UnmapViewOfFile来释放内存区域 ~~~c++ BOOL UnmapViewOfFile(PVOID pvBaseAddress); ~~~ pvBaseAddress指定了要返还区域的基地址,必须和MapViewOfFile的返回值相同 2. 关闭文件映射内核对象 3. 关闭文件内核对象 和第2步一样都是CloseHandle ### 用内存映射文件在进程间共享数据 Windows提供了多种机制在应用程序间快速、方便地共享数据和信息,包括RPC、COM、OLE、DDE、窗口消息、剪贴板、邮槽、管道、套接字等。在WIndows中,在同一台机器上共享数据最底层的机制就是内存映射文件。 这种数据共享机制是通过让两个或更多进程映射同一个文件映射对象的视图来实现的,这意味着它们共享相同的物理存储页面。所以,一个进程在共享文件映射对象的视图中写入数据时,其他进程也可以看到变化(注意所有进程必须为文件映射对象使用完全相同的名称) ## 堆 堆非常适合分配大量小数据块。例如,链表和树最好是用堆来管理,优点在于不必理会分配粒度和页面边界,缺点在于分配和释放内存块的速度比其他方式慢,而且无法再对物理存储的调拨和撤销调拨进行直接控制 略过,不太重要 最后修改:2026 年 01 月 11 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏