C++ 文件映射:在文件内创建视图
前言:
通常,经过磁盘的IO是异常缓慢的,使用 ReadFile() 来读取文件效率低下。因为 ReadFile() 的工作流程是这样的:
磁盘文件(慢速) –> 拷贝至内存缓冲区 –> 业务逻辑:读取/使用(第2次拷贝)
直接使用 ReadFile() 确实方便,但当遇到超大文件业务处理或者是IO密集型业务的时候就显得力不从心了。那么,Windows 为我们提供了 FileMapping 这样一种概念(Linux 平台称作 “mmap”),来将文件直接映射至内存的某一片区域,上层应用通过内存操作,直接存取数据,其流程如下:
内存(磁盘文件) <–内存操作–> 业务逻辑
这样一来,有效解决了IO操作的效率。这次,我们讨论在文件内建立内存映射视图。
场景:
如果我们想要浏览一个并非从文件起始位置开始的文件视图,我们首先必须要创建文件映射对象。这个对象的大小就是该文件中你想要预览的内容外加偏移大小。例如,如果我们需要预览一个文件内从第 131,072 字节(128 KB) 开始向后 1 KB 字节的内容,我们必须要创建一个至少 132,096 字节(129 KB) 的内存映射视图。这个视图从文件内第 131,072 字节(128 KB) 开始并延伸至少 1,024 字节。本实例假定操作系统文件分配颗粒为 64 KB。(实际上 Windows 系统给出的默认值便是 64 KB)
文件映射块影响着我们视图的开始位置。一个文件视图的起始位置必须是分配颗粒(64 KB)的整数倍。因此,我们想要查看的数据应该是视图内文件偏移与分配块求模。视图的大小就是数据与分配颗粒求模的偏移,外加上你想要使用的数据大小。
举个例子,我们使用 GetSystemInfo() 这样一个 API 来确定到分配颗粒的大小实际上就是 64 KB。因此,我们要通过内存映射来使用一个 138,240 字节(135 KB) 文件中的 1KB 数据,分为以下几件事:
- 创建一个至少 139,264 字节(136 KB) 的内存映射对象。
- 计算偏移量,文件偏移量是文件分配粒度小于所需偏移量的最大倍数。文件从偏移量开始起始。在本例中,视图从 128 KB 起始。视图的大小便是 136 KB – 128 KB (8 KB)。
- 在视图内创建一个偏移量为 7 KB 的指针指向我们感兴趣的 1 KB 的内容。
如果我们想要访问的数据超越了分配颗粒边界,我们应该创建一个大于分配颗粒大小的视图。这避免将数据打散成零散的碎片部分。
下面是上面例子的 C++ 代码实现。
代码清单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
/* 这份代码演示了文件映射,特别是使用分配颗粒来关联视图。 */ #include <windows.h> #include <stdio.h> #include <tchar.h> #define BUFFSIZE 1024 // 每次想要访问的文件数据大小 #define FILE_MAP_START 138240 // 要读数据的文件的起始位置点 (135K) /* 测试文件。下面的代码创建并填充这份文件。 因此,不需要事先准备好该文件。 */ TCHAR * lpcTheFile = TEXT("fmtest.txt"); // 要填充的文件 int main(void) { HANDLE hMapFile; // 文件映射句柄 HANDLE hFile; // 文件句柄 BOOL bFlag; // 结果标志位 DWORD dBytesWritten; // 实际写入字节数 DWORD dwFileSize; // 临时用来存放文件大小 DWORD dwFileMapSize; // 文件映射的大小 DWORD dwMapViewSize; // 文件映射视图的大小 DWORD dwFileMapStart; // 文件映射视图的起始位置 DWORD dwSysGran; // 分配颗粒大小 SYSTEM_INFO SysInfo; // 系统信息结构,用来获取分配颗粒的大小 LPVOID lpMapAddress; // 指向文件映射基址的指针 char * pData; // 指向数据的指针 int i; // 循环计数 int iData; // 成功时,包含了数据的第一个 int。 int iViewDelta; // 视图内数据起始位置偏移 // 创建测试文件。 使用 "Create Always" 来覆盖任何存在的文件, // 数据会在下面重新创建。 hFile = CreateFile(lpcTheFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { _tprintf(TEXT("文件句柄是 NULL。\n")); _tprintf(TEXT("目标文件:%s\n"), lpcTheFile); return 4; } // 获得系统分配颗粒大小 GetSystemInfo(&SysInfo); dwSysGran = SysInfo.dwAllocationGranularity; // 现在来计算一些变量值。文件偏移用64位值来计算,然后将低32位用于函数调用。 // 要计算文件映射从哪开始, 将数据的偏移四舍五入至最接近分配颗粒的数值。 dwFileMapStart = (FILE_MAP_START / dwSysGran) * dwSysGran; _tprintf (TEXT("文件映射从文件内第 %ld 字节开始。\n"), dwFileMapStart); // 计算文件映射视图大小。 dwMapViewSize = (FILE_MAP_START % dwSysGran) + BUFFSIZE; _tprintf (TEXT("文件映射视图大小:%ld 字节。\n"), dwMapViewSize); // 文件映射对象有多大呢? dwFileMapSize = FILE_MAP_START + BUFFSIZE; _tprintf (TEXT("文件映射对象大小:%ld 字节。\n"), dwFileMapSize); // 我们想要的数据并不在文件映射视图的起始位置,因此我们要计算偏移。 iViewDelta = FILE_MAP_START - dwFileMapStart; _tprintf (TEXT("数据在视图里面 %d 字节处。\n"), iViewDelta); // 现在创建一个含有合适数据的文件用来测试。 // 我们在文件内提供单独的4字节偏移来方便观察。 // 注意,代码不对存储数据做完整性检查! // 一个整形(INT)是 4 字节的, 因此,指向所需数据的指针应该是文件中所需偏移量的四分之一。 for (i=0; i<(int)dwSysGran; i++) { WriteFile (hFile, &i, sizeof (i), &dBytesWritten, NULL); } // 验证是否正确写入这么多字节。 dwFileSize = GetFileSize(hFile, NULL); _tprintf(TEXT("文件大小:%10d\n"), dwFileSize); // 为文件创建内存映射对象。 // 强烈建议首先判断文件是否是空的(0字节),因为空文件映射到内存当中毫无意义。 hMapFile = CreateFileMapping( hFile, // 当前的文件句柄 NULL, // 默认安全结构 PAGE_READWRITE, // 给予读写权限 0, // 文件映射对象大小,高32位 dwFileMapSize, // 文件映射对象大小,低32位 NULL); // 文件映射名称,此处灵活使用可以实现跨进程通信。 if (hMapFile == NULL) { _tprintf(TEXT("文件映射句柄为 NULL:Last Error:%d\n"), GetLastError() ); return (2); } // 映射视图,并测试结果。 lpMapAddress = MapViewOfFile(hMapFile, // 文件映射对象句柄 FILE_MAP_ALL_ACCESS, // 读写权限 0, // 文件偏移大小高32位 dwFileMapStart, // 文件偏移大小低32位 dwMapViewSize); // 要映射的视图大小 if (lpMapAddress == NULL) { _tprintf(TEXT("文件映射视图基址为 NULL:Last Error:%d\n"), GetLastError()); return 3; } // 计算数据实际位置 pData = (char *) lpMapAddress + iViewDelta; // 抽取数据,pData 是字节型指针,先强制转换为整形指针,再获取其值。 iData = *(int *)pData; _tprintf (TEXT("在当前指针处的数据是 %d,\n%s文件偏移的四分之一处。\n"), iData, iData*4 == FILE_MAP_START ? TEXT("在") : TEXT("不在")); // 关闭文件映射和打开的文件 bFlag = UnmapViewOfFile(lpMapAddress); bFlag = CloseHandle(hMapFile); // 关闭文件映射对象 if(!bFlag) { _tprintf(TEXT("\n发生错误:%ld,无法关闭文件映射对象。"), GetLastError()); } bFlag = CloseHandle(hFile); // 关闭文件本身 if(!bFlag) { _tprintf(TEXT("\n发生错误:%ld,无法关闭文件。"), GetLastError()); } return 0; } |
使用文件映射确实挺复杂,要读取数据,首先得计算各类偏移,还要判断读取位置是否越界,遇到超长文件更需要做分段映射操作。相比 ReadFile() 可是麻烦了几个数量级。但是,对于IO密集的业务来说,优质的IO操作代码,还是必要的。
参考文献:
- MSDN, Creating a View Within a File, https://msdn.microsoft.com/zh-cn/library/windows/desktop/aa366548(v=vs.85).aspx