前言:
通常,经过磁盘的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++ 代码实现。
代码清单:
/* 这份代码演示了文件映射,特别是使用分配颗粒来关联视图。 */ #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
Leave a Reply