第14章:内存映射文件
引言
内存映射文件(Memory-Mapped File,MMF,在内核中称为"节",Section)是 Windows 提供的一种强大机制,它将磁盘文件的内容映射到进程的虚拟地址空间中,使应用程序可以像访问普通内存一样访问文件数据。当加载 EXE 或 DLL 时,操作系统正是通过内存映射文件将其映射到内存中执行的——对底层文件的访问通过标准指针间接完成。代码执行时,首次访问某一内存地址会触发页面错误(Page Fault),由内存管理器(Memory Manager)从文件中读取对应数据放入物理内存。
可以将内存映射文件想象成在磁盘文件和进程内存之间架设了一座"透明的传送带":你看到的是连续的内存区域,但实际数据则按需从文件自动加载,无需手动搬运。
相较于传统的 ReadFile / WriteFile 方式,使用内存映射文件操作数据有显著优势:
- 无需手动分配缓冲区、无需反复调用读写函数,文件指针的移动转化为直观的指针运算
- 所有标准内存操作函数(如
memcpy、memset)均可直接应用于映射区域 - 操作系统负责缓存管理,多次访问同一数据时性能更高
- 天然支持进程间共享内存,是最快的 IPC(Inter-Process Communication,进程间通信)方式
映射文件
创建文件映射对象
将现有文件映射到内存的第一步是通过 CreateFile 打开目标文件,访问掩码(Access Mask)必须与后续所需的读写权限匹配。之后调用 CreateFileMapping(创建文件映射)创建文件映射对象(File Mapping Object):
HANDLE CreateFileMapping(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCTSTR lpName);参数说明:
| 参数 | 说明 |
|---|---|
hFile | 有效的文件句柄;若用于页面文件(Paging File)支持的共享内存,则传入 INVALID_HANDLE_VALUE |
lpFileMappingAttributes | 安全属性(Security Attributes),通常为 NULL |
flProtect | 页面保护(Page Protection)方式,决定映射区域的读写执行权限 |
dwMaximumSizeHigh | 映射对象最大大小的高 32 位 |
dwMaximumSizeLow | 映射对象最大大小的低 32 位 |
lpName | 可选名称,便于跨进程共享该映射对象 |
表 14-1:CreateFileMapping 的保护标志
| MMF 保护标志 | 文件最小访问标志 | 注释 |
|---|---|---|
PAGE_READONLY | GENERIC_READ | 不允许写入操作 |
PAGE_READWRITE | GENERIC_READ + GENERIC_WRITE | — |
PAGE_WRITECOPY | GENERIC_READ | 写时复制(Copy-on-Write),修改不会写回原文件 |
PAGE_EXECUTE_READ | GENERIC_READ + GENERIC_EXECUTE | — |
PAGE_EXECUTE_READWRITE | 三者皆需 | — |
PAGE_EXECUTE_WRITECOPY | GENERIC_READ + GENERIC_EXECUTE | 等效于 PAGE_EXECUTE_READ |
保护标志还可与以下标志组合使用:SEC_COMMIT(立即提交)、SEC_RESERVE(仅预留,第 13 章已介绍)、SEC_IMAGE(映像文件映射,用于 PE 文件)、SEC_LARGE_PAGES(大页面)等。
大小参数由两个 32 位值拼接为 64 位值。以只读方式映射现有文件时,通常将其设为零,此时 MMF 大小自动等于文件大小。若需要写入,则应将大小设为文件可能达到的最大值,以便为扩展预留空间。命名参数类似于其他内核对象(如事件、信号量),便于跨进程标识同一对象。
以下示例以只读方式创建文件映射对象(省略错误处理):
HANDLE hFile = ::CreateFile(L"c:\\mydata.dat", GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
HANDLE hMemFile = ::CreateFileMapping(hFile, nullptr,
PAGE_READONLY, 0, 0, nullptr);
::CloseHandle(hFile);注意,创建 MMF 后可以安全地关闭原始文件句柄——CreateFileMapping 内部会复制一份文件句柄,确保在 MMF 对象存活期间文件不会被意外关闭。
映射视图
创建 MMF 对象只是获得了内核对象的句柄,要让数据出现在进程地址空间中,还需要调用 MapViewOfFile(映射文件视图)将文件内容映射到虚拟地址空间的某个区域:
LPVOID MapViewOfFile(
_In_ HANDLE hFileMappingObject,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD dwFileOffsetLow,
_In_ SIZE_T dwNumberOfBytesToMap);参数说明:
hFileMappingObject:由CreateFileMapping返回的句柄dwDesiredAccess:期望的访问权限,必须与创建时指定的保护标志兼容dwFileOffsetHigh/dwFileOffsetLow:映射起始偏移量的高/低 32 位,必须是 64KB(分配粒度,Allocation Granularity)的倍数dwNumberOfBytesToMap:映射的字节数,设为 0 表示映射到文件末尾
表 14-2:MapViewOfFile 的访问标志
| 期望访问权限 | 描述 |
|---|---|
FILE_MAP_READ | 读访问 |
FILE_MAP_WRITE | 写访问 |
FILE_MAP_EXECUTE | 执行访问 |
FILE_MAP_ALL_ACCESS | 等效于 FILE_MAP_WRITE |
FILE_MAP_COPY | 写时复制(Copy-on-Write),取消映射时丢弃更改 |
FILE_MAP_LARGE_PAGES | 大页面映射 |
FILE_MAP_TARGETS_INVALID | CFG(Control Flow Guard)无效目标 |
将 dwNumberOfBytesToMap 设为 0 是一种常见做法,表示映射从指定偏移开始直到文件末尾的全部内容。映射成功后返回的指针指向映射区域的起始地址,可像普通内存一样进行读写。
使用完毕后,通过 UnmapViewOfFile 取消映射:
BOOL UnmapViewOfFile(_In_ LPCVOID lpBaseAddress);取消映射后,该指针所指内存不再有效,任何后续访问将导致访问冲突(Access Violation)。应在不再需要数据时及时取消映射以释放地址空间资源。
filehist 应用程序
filehist 是一个命令行工具,用于统计文件中每个字节值(0–255)的出现频率。该程序通过内存映射文件实现,能够处理任意大小的文件——采用"分块映射、处理、取消映射、再处理下一块"的策略,避免了为超大型文件一次性映射全部内容可能导致的地址空间压力。
用法示例:
C:\>filehist.exe
Usage: filehist [view size in MB] <file path>
Default view size is 10 MB处理大文件的输出示例(片段):
File size: 938857496 bytes
Using view size: 10 MB
Mapping offset: 0x0, size: 0xA00000 bytes
...
0xFF: 72242071 ( 7.69 %)
0x00: 342452879 (36.48 %)核心实现
入口函数解析命令行参数,确定视图大小(默认 10MB):
int wmain(int argc, const wchar_t* argv[]) {
if (argc < 2) {
printf("Usage:\tfilehist [view size in MB] <file path>\n");
printf("\tDefault view size is 10 MB\n");
return 0;
}
DWORD viewSize = argc == 2 ? (10 << 20) : (_wtoi(argv[1]) << 20);
if (viewSize == 0) viewSize = 10 << 20;定义数据结构存储每个字节值及其出现次数:
struct Data {
BYTE Value;
long long Count;
};
Data count[256] = { 0 };
for (int i = 0; i < 256; i++) count[i].Value = i;打开文件并创建 MMF:
HANDLE hFile = ::CreateFile(argv[argc - 1], GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return Error("Failed to open file");
LARGE_INTEGER fileSize;
if (!::GetFileSizeEx(hFile, &fileSize))
return Error("Failed to get file size");
HANDLE hMapFile = ::CreateFileMapping(hFile, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (!hMapFile)
return Error("Failed to create MMF");
::CloseHandle(hFile);核心循环:分块映射、统计、取消映射、推进偏移量:
auto total = fileSize.QuadPart;
printf("File size: %llu bytes\n", fileSize.QuadPart);
printf("Using view size: %u MB\n", (unsigned)(viewSize >> 20));
LARGE_INTEGER offset = { 0 };
while (fileSize.QuadPart > 0) {
auto mapSize = (unsigned)min(viewSize, fileSize.QuadPart);
printf("Mapping offset: 0x%llX, size: 0x%X bytes\n", offset.QuadPart, mapSize);
auto p = (const BYTE*)::MapViewOfFile(hMapFile, FILE_MAP_READ,
offset.HighPart, offset.LowPart, mapSize);
if (!p)
return Error("Failed in MapViewOfFile");
for (DWORD i = 0; i < mapSize; i++)
count[p[i]].Count++;
::UnmapViewOfFile(p);
offset.QuadPart += mapSize;
fileSize.QuadPart -= mapSize;
}
::CloseHandle(hMapFile);统计完成后排序并输出结果:
std::sort(std::begin(count), std::end(count),
[](const auto& c1, const auto& c2) { return c2.Count > c1.Count; });
for (const auto& data : count) {
printf("0x%02X:%10llu(%5.2f%%)\n", data.Value, data.Count,
data.Count * 100.0 / total);
}这个示例展示了内存映射文件处理大型文件的典型模式:通过分块映射(Chunked Mapping)兼顾内存效率与编程便利性。
共享内存
在 Windows 中,每个进程拥有独立的虚拟地址空间,彼此隔离。但实际开发中经常需要在进程间共享数据。Windows 提供了多种 IPC 机制——COM、Windows 消息、套接字(Socket)、管道(Pipe)等——但它们都需要将数据从一个进程的地址空间复制到另一个进程的地址空间。内存映射文件则是最快的 IPC 方式:无需数据复制。一个进程写入共享内存后,所有拥有同一文件映射对象句柄的进程可立即看到变更。
进程间共享可通过命名(Naming)机制实现。共享内存有两种后备方式:
- 文件备份(File-Backed):数据持久化到磁盘文件,即使所有进程退出、MMF 销毁后数据仍然存在
- 分页文件备份(Pagefile-Backed):由系统分页文件(Paging File)支持,MMF 销毁后数据被丢弃
基本共享示例
"基本共享"(Basic Sharing)应用程序演示了命名共享内存的使用:一个进程写入数据,另一个进程读取数据并显示相同内容。
在对话框初始化时创建 MMF(以页面文件为后备):
m_hSharedMem = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, 1 << 12, L"MySharedMemory");大小为 4KB,可读写。将 hFile 参数设为 INVALID_HANDLE_VALUE 表示由分页文件支持,大小参数不可为零。首次调用会创建新的文件映射对象;后续以相同名称的调用仅返回现有对象的句柄——此时传入的其他参数实际无效。
也可以使用 OpenFileMapping 打开已存在的命名映射对象(若不存在则失败):
HANDLE OpenFileMapping(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCTSTR lpName);写入数据的逻辑:映射视图、写入文本、取消映射:
LRESULT CMainDlg::OnWrite(WORD, WORD, HWND, BOOL &) {
void* buffer = ::MapViewOfFile(m_hSharedMem, FILE_MAP_WRITE, 0, 0, 0);
if (!buffer) {
AtlMessageBox(m_hWnd, L"Failed to map memory", IDR_MAINFRAME);
return 0;
}
CString text;
GetDlgItemText(IDC_TEXT, text);
::wcscpy_s((PWSTR)buffer, text.GetLength() + 1, text);
::UnmapViewOfFile(buffer);
return 0;
}读取数据的逻辑:映射视图、读取文本、显示到界面:
LRESULT CMainDlg::OnRead(WORD, WORD, HWND, BOOL &) {
void* buffer = ::MapViewOfFile(m_hSharedMem, FILE_MAP_READ, 0, 0, 0);
if (!buffer) {
AtlMessageBox(m_hWnd, L"Failed to map memory", IDR_MAINFRAME);
return 0;
}
SetDlgItemText(IDC_TEXT, (PCWSTR)buffer);
::UnmapViewOfFile(buffer);
return 0;
}memview 监控应用
另一个独立进程 memview 使用 OpenFileMapping 打开相同的命名 MMF,定期轮询读取数据并检测变化:
int main() {
HANDLE hMemMap = ::OpenFileMapping(FILE_MAP_READ, FALSE, L"MySharedMemory");
if (!hMemMap) {
printf("File mapping object is not available\n");
return 1;
}
WCHAR text[1024] = { 0 };
auto data = (const WCHAR*)::MapViewOfFile(hMemMap, FILE_MAP_READ, 0, 0, 0);
if (!data) {
printf("Failed to map shared memory\n");
return 1;
}
for (;;) {
if (::_wcsicmp(text, data) != 0) {
::wcscpy_s(text, data);
printf("%ws\n", text);
}
::Sleep(1000);
}
}通过进程资源管理器(Process Explorer)可以查看 "MySharedMemory" 文件映射对象的句柄数量,该数值反映了当前使用该共享内存的进程总数。
带文件后备的共享内存
"基本共享增强版"(Enhanced Basic Sharing)应用程序演示了由指定文件备份的共享内存。用户可在界面上指定文件名,或留空以使用分页文件后备。
"创建"按钮的处理逻辑:
LRESULT CMainDlg::OnCreate(WORD, WORD, HWND, BOOL&) {
CString filename;
GetDlgItemText(IDC_FILENAME, filename);
HANDLE hFile = INVALID_HANDLE_VALUE;
if (!filename.IsEmpty()) {
hFile = ::CreateFile(filename, GENERIC_READ | GENERIC_WRITE, 0,
nullptr, OPEN_ALWAYS, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
AtlMessageBox(*this, L"Failed to create/open file",
IDR_MAINFRAME, MB_ICONERROR);
return 0;
}
}
m_hSharedMem = ::CreateFileMapping(hFile, nullptr, PAGE_READWRITE,
0, 1 << 12, L"MySharedMemory");
if (!m_hSharedMem) {
AtlMessageBox(m_hWnd, L"Failed to create shared memory",
IDR_MAINFRAME, MB_ICONERROR);
EndDialog(IDCANCEL);
}
if (hFile != INVALID_HANDLE_VALUE)
::CloseHandle(hFile);
EnableUI();
return 0;
}若指定的文件不存在,OPEN_ALWAYS 标志会自动创建文件,且 MMF 会将其大小扩展至 4KB(即创建时指定的大小)。应用程序启动时,先通过 OpenFileMapping 检查命名对象是否已存在——若存在则直接进入编辑模式,无需重复创建。
文件后备的共享内存适合需要数据持久化的场景:即使所有进程退出,数据仍保留在文件中,下次启动时可重新映射并读取。
微型 Excel 2 应用程序
该应用程序将第 13 章介绍的预留(Reserve)大块内存的技术与内存映射文件相结合,使预留区域成为可在进程间共享的内存。关键点在于 CreateFileMapping 中使用 SEC_RESERVE 标志:
bool CMainDlg::AllocateRegion() {
m_hSharedMem = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE | SEC_RESERVE, TotalSize >> 32, (DWORD)TotalSize,
L"MicroExcelMem");
if (!m_hSharedMem) {
AtlMessageBox(nullptr, L"Failed to create shared memory",
IDR_MAINFRAME, MB_ICONERROR);
EndDialog(IDCANCEL);
return false;
}
m_Address = ::MapViewOfFile(m_hSharedMem, FILE_MAP_READ | FILE_MAP_WRITE,
0, 0, TotalSize);
CString addr;
addr.Format(L"0x%p", m_Address);
SetDlgItemText(IDC_ADDRESS, addr);
SetDlgItemText(IDC_CELLADDR, addr);
return true;
}TotalSize 设为 1GB。由于使用了 SEC_RESERVE 标志,整个区域仅被预留(地址空间被保留)而非实际提交物理内存。首次向某个单元格写入数据时会触发访问冲突异常(Access Violation Exception),异常处理函数捕获后按需提交页面:
LRESULT CMainDlg::OnWrite(WORD, WORD, HWND, BOOL&) {
int x, y;
auto p = GetCell(x, y);
if (!p) return 0;
WCHAR text[512];
GetDlgItemText(IDC_TEXT, text, _countof(text));
try {
::wcscpy_s((WCHAR*)p, CellSize / sizeof(WCHAR), text);
}
except (FixMemory(p, GetExceptionCode())) {
// 永远不会执行到这里——FixMemory 返回 EXCEPTION_CONTINUE_EXECUTION 后重试成功
}
return 0;
}
int CMainDlg::FixMemory(void* address, DWORD exceptionCode) {
if (exceptionCode == EXCEPTION_ACCESS_VIOLATION) {
::VirtualAlloc(address, CellSize, MEM_COMMIT, PAGE_READWRITE);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}使用 SEH(Structured Exception Handling,结构化异常处理)的 __try/__except 机制:写入未提交页面时触发 EXCEPTION_ACCESS_VIOLATION,FixMemory 调用 VirtualAlloc 提交该单元格所在的页面(CellSize 字节),然后返回 EXCEPTION_CONTINUE_EXECUTION 让系统重试引发异常的指令——这次写入将成功。
运行第二个实例时,两个进程会看到完全相同的共享内存数据。即使映射地址不同,也不影响数据一致性——因为所有视图映射到的是同一物理内存。在 VMMap 工具中,这类内存的类型显示为 "Shareable"(可共享)。
其他内存映射函数
Windows 在后续版本中不断扩展内存映射 API,提供了更精细的控制能力。
MapViewOfFileEx
允许调用者指定目标映射地址(必须是 64KB 的倍数):
LPVOID MapViewOfFileEx(
_In_ HANDLE hFileMappingObject,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwFileOffsetHigh,
_In_ DWORD dwFileOffsetLow,
_In_ SIZE_T dwNumberOfBytesToMap,
_In_opt_ LPVOID lpBaseAddress);通常将 lpBaseAddress 设为 NULL 让系统自动选择地址。指定地址的典型场景是 PE 映像映射(配合 SEC_IMAGE 使用),因为 PE 文件中可能包含指向映像内其他位置的固定地址指针,需要在期望的基址加载。对于一般数据共享,建议在共享内存中存储偏移量(Offset)而非绝对地址,以兼容不同进程中可能不同的映射地址。
MapViewOfFile2(Windows 10 1703 引入)
PVOID MapViewOfFile2(
_In_ HANDLE FileMappingHandle,
_In_ HANDLE ProcessHandle,
_In_ ULONG64 Offset,
_In_opt_ PVOID BaseAddress,
_In_ SIZE_T ViewSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection);新特性:可指定目标进程句柄(ProcessHandle),允许一个进程为另一个进程映射视图;偏移量使用单个 64 位值(ULONG64),参数更简洁。AllocationType 可为 0、MEM_RESERVE 或 MEM_LARGE_PAGES。该函数通过 MapViewOfFileNuma2 实现,需链接 mincore.lib 导入库。
UnmapViewOfFile2
BOOL UnmapViewOfFile2(
_In_ HANDLE Process,
_In_ PVOID BaseAddress,
_In_ ULONG UnmapFlags);允许取消映射其他进程中的视图。UnmapFlags 通常为零。另有 UnmapViewOfFileEx 变体,始终作用于调用进程自身。
UWP 版本为 MapViewOfFile2FromApp,由 MapViewOfFile2 内联调用。
MapViewOfFile3(Windows 10 1804 引入)
PVOID MapViewOfFile3(
_In_ HANDLE FileMapping,
_In_opt_ HANDLE Process,
_In_opt_ PVOID BaseAddress,
_In_ ULONG64 Offset,
_In_ SIZE_T ViewSize,
_In_ ULONG AllocationType,
_In_ ULONG PageProtection,
_Inout_ MEM_EXTENDED_PARAMETER* ExtendedParameters,
_In_ ULONG ParameterCount);这是最具灵活性的"超级函数",通过 MEM_EXTENDED_PARAMETER 结构数组可指定扩展属性(与第 13 章的 VirtualAlloc2 用法一致),例如 NUMA 节点偏好、特定内存属性等。UWP 版本为 MapViewOfFile3FromApp。
CreateFileMapping2(Windows 10 1809 引入)
HANDLE CreateFileMapping2(
_In_ HANDLE File,
_In_opt_ SECURITY_ATTRIBUTES* SecurityAttributes,
_In_ ULONG DesiredAccess,
_In_ ULONG PageProtection,
_In_ ULONG AllocationAttributes,
_In_ ULONG64 MaximumSize,
_In_opt_ PCWSTR Name,
_Inout_updates_opt_(ParameterCount) MEM_EXTENDED_PARAMETER* ExtendedParameters,
_In_ ULONG ParameterCount);同样支持 MEM_EXTENDED_PARAMETER 数组,允许创建时指定扩展属性。以下示例在 NUMA 节点 1 上创建映射对象:
HANDLE hFile = ...;
MEM_EXTENDED_PARAMETER param = { 0 };
param.Type = MemExtendedParameterNumaNode;
param.ULong = 1; // NUMA node 1
HANDLE hMemMap = ::CreateFileMapping2(hFile, nullptr, FILE_MAP_READ,
PAGE_READONLY, 0, 0, nullptr, ¶m, 1);| 函数 | 引入版本 | 关键增强 |
|---|---|---|
CreateFileMapping | 原始 API | 基础文件映射对象创建 |
MapViewOfFile | 原始 API | 基础视图映射 |
MapViewOfFileEx | 原始 API | 可指定目标映射地址 |
MapViewOfFile2 | Win10 1703 | 跨进程映射、64 位偏移量 |
MapViewOfFile3 | Win10 1804 | 支持 MEM_EXTENDED_PARAMETER 扩展属性 |
CreateFileMapping2 | Win10 1809 | 创建时支持扩展属性(如 NUMA 节点) |
数据一致性
文件映射对象在数据一致性方面提供了以下保证与限制:
多视图的一致性
同一文件映射对象的多个视图保证同步——无论这些视图来自同一进程还是不同进程。因为它们映射到的是相同的物理内存页面,任何进程的写入操作在其他进程的视图中立即可见,无需显式刷新(Flush)。
可以通过内存映射机制自己给自己发消息:在共享内存中建立一个循环队列(Circular Queue),配合命名事件(Named Event)对象发出通知,即可实现高效的单向或双向进程间通信。
例外情况
- 网络远程文件:映射位于远程机器上的文件时(例如通过 UNC 路径访问),不同机器上的视图可能不同步。这是因为网络文件系统的缓存语义与本地文件系统不同。
- 多个文件映射对象映射同一文件:Windows 不保证两个不同的文件映射对象映射同一文件时的数据同步。通常应避免使用两个以上的 MMF 映射同一文件。如需在多个 MMF 场景下使用同一文件,建议以独占访问方式(Exclusive Access)打开文件。
与普通 I/O 混用
若文件已被 MMF 映射,同时又被以普通 I/O 方式(ReadFile / WriteFile)打开并操作,I/O 操作的更改不会立即反映在映射视图中。这是因为 MMF 和普通 I/O 使用不同的缓冲机制。应避免对同一文件混合使用两种访问方式;若必须混合,需在 I/O 操作后调用 FlushViewOfFile 或 FlushFileBuffers 手动同步。
总结
内存映射文件是 Windows 中最灵活且高效的功能之一。本章涵盖的核心要点:
映射文件:通过
CreateFileMapping创建映射对象,MapViewOfFile将其映射到进程地址空间。分块映射策略可处理超大型文件。共享内存:通过命名 MMF 实现,是最快的 IPC 方式——零数据复制。可由页面文件或磁盘文件后备。
预留+按需提交:结合
SEC_RESERVE标志与 SEH 异常处理(第 13 章技术),实现跨进程的稀疏内存分配。扩展 API:Windows 10 持续引入
MapViewOfFile2/3和CreateFileMapping2,支持 NUMA 感知、跨进程映射和扩展属性。数据一致性:同一 MMF 对象的视图保证同步,但跨网络或混合 I/O 场景需要特别注意。
内存映射文件不仅在应用程序开发中至关重要,也是操作系统加载可执行映像、实现 DLL 共享等底层机制的基础,是理解 Windows 内存管理体系的关键环节。下一章将讨论动态链接库(DLL)。