17.5 稀疏调拨的内存映射文件--《Windows核心编程》
原文链接:https://www.likecs.com/show-306421749.html,原文中代码是C++MFC程序,更详细。本文是C语言测试代码。
(1)稀疏文件(Sparse File)定义
指的是文件中出现大量的0数据,这些数据对我们用处不大,但是却一样的占用空间。NTFS文件系统对此进行了优化,那些无用的0字节被用一定的算法压缩起来。例如声明一个很大的稀疏文件(如100GB),这个文件实际上并不需要占用那么大的空,内部都是一些无用的0数据,那么NTFS就会利用算法释放这些无用的0字节空间,这是对磁盘占用空间的一种优化。但要注意FAT32并不支持稀疏文件的压缩。
实例场景:假设我们要创建一个内存映射文件来存储录音的数据。当用户说话的时候,我们把数字音频数据写入到内存缓存,并以磁盘文件作为内存缓存的后备存储器。一个部分调拨的内存映射文件当然是最简单和最高效的方式。问题在于我们并不知道用户在停止录制之前会说多久,可能是五分钟,可能是五小时…这差距还是很大的。所以我们需要一个足够大的文件来保存这些数据。单是,在使用稀疏调拨的内存映射文件时,大小并没有多大关系。
(2)与稀疏文件操作有关的函数
①判断系统是否支持稀疏文件:GetVolumeInformation 函数
通过传出的参数lpFileSystemFlags & FILE_SUPPORTS_SPARSE_FILES判断结果是否为FILE_SUPPORTS_SPARSE_FILES。
②判断一个文件是否是稀疏文件:GetFileInformationByHandle 函数
BY_HANDLE_FILE_INFORMATION stFileInfo;
GetFileInformationByHandle(hFile, &stFileInfo);
当stFileInfo.dwFileAttributes & FILE_ATTRIBUTE_SPARSE_FILE为TRUE时表示稀疏文件。
③产生一个稀疏文件:DeviceIoControl(hFile,FSCTL_SET_SPARS,...);
大部分文件,在改变它的EndOfFile的时候,中间的空白会被操作系统填0,也就是说,如果用SetFilePointer和SetEndOfFile来产生一个很大的文件,那么这个文件它占用的是真正的磁盘空间,即使里面全是0,系统默认的也会在DeviceIoControl()中的ControlCode里用FSCTL_SET_ZERO_DATA标记,这个标记使得那些文件空洞被0所填充。为了节省磁盘空间,我们必须把一个文件声明为稀疏文件,以便让系统把那些无用的0字节压缩,并释放相应的磁盘空间,要将标记改为FSCTL_SET_SPARSE。
④查找稀疏文件中包含非零数据的范围:DeviceIoControl(hFile, FSCTL_QUERY_ALLOCATED_RANGES,...);
(3)以稀疏文件为后备存储器的内存映射文件(一般的使用步骤)
① 创建文件:hFile = CreateFile(...);
② 产生稀疏文件
DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dw, NULL));
③ 创建内存映射文件对象
hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE,…);
④ 映射到进程的地址空间
pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, 0);
⑤ 读、写文件(像直接读写内存一样的读写文件)
⑥ 撤消映射:UnmapViewOfFile(pvFile);
⑦ 关闭句柄:CloseHandle(hFileMap);CloseHandle(hFile);
#include <windows.h>
#include <iostream>
#include <string>
#include <winioctl.h>
using namespace std;
#define FL (1024*1024)
#define FH 0
char g_szPath[MAX_PATH];
char g_Volume[MAX_PATH];
//(3797960)
int main()
{
// 先检查当前磁盘是否支持稀疏文件
memset(g_Volume, 0, MAX_PATH);
GetCurrentDirectory(MAX_PATH, g_szPath);
strncpy_s(g_Volume, g_szPath, 3);
DWORD dwFileSystemFlags = 0;
bool bOK = GetVolumeInformation(g_Volume, NULL, 0, NULL, NULL, &dwFileSystemFlags, NULL, 0);// 获取磁盘信息
bOK = bOK && (dwFileSystemFlags & FILE_SUPPORTS_SPARSE_FILES);// dwFileSystemFlags & FILE_SUPPORTS_SPARSE_FILES 查看标志位判断对稀疏文件的支持
if (!bOK)
{
// 如果系统不支持稀疏文件,就直接关闭程序
return 1;
}
// ① 创建文件
DWORD dw;
string FileName = "MMF.txt";
HANDLE hFile = CreateFile(FileName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
int err = GetLastError();
if (hFile == INVALID_HANDLE_VALUE)
{
cout << "打开文件失败" << endl;
return 1;
}
// ② 产生稀疏文件
bool ret = DeviceIoControl(hFile, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &dw, NULL);// 设置稀疏文件
err = GetLastError();
if (!ret)
{
cout << "err no:" << err << endl;
return 2;
}
// ③ 创建内存映射文件对象
HANDLE hFileMapping = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 1024 * 1024, NULL); // 申请1M,但实际测试只有64KB
if (!hFileMapping)
{
CloseHandle(hFileMapping);
cout << "创建文件映像失败" << endl;
return 2;
}
// ④ 映射到进程的地址空间
LPVOID pFile = MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
// ⑤ 读、写文件(像直接读写内存一样的读写文件)
char WriteIn[] = "写入测试";
memcpy((LPVOID)((INT64)pFile + 0x120), WriteIn, strlen(WriteIn) + 1);
FlushViewOfFile(pFile, 4); // 不强制刷新的话,后面获取文件磁盘占用大小会失败,因为还没刷新到文件中。
LARGE_INTEGER lpFileSize;
GetFileSizeEx(hFile, &lpFileSize);
printf("文件实际大小(压缩前大小):\n%lld bytes\n%4.2f KB\n%4.2f MB\n%4.2f GB\n", lpFileSize.QuadPart, (float)lpFileSize.QuadPart / 1024, (float)lpFileSize.QuadPart / (1024 * 1024), (float)lpFileSize.QuadPart / (1024 * 1024 * 1024));
ULARGE_INTEGER ulFileSize;
ulFileSize.LowPart = GetCompressedFileSize("C:\\Users\\Lenovo\\source\\repos\\test2333\\test2333\\MMF.txt", &ulFileSize.HighPart);
err = GetLastError();
printf("文件磁盘大小\n%lld bytes\n%4.2f KB\n%4.2f MB\n%4.2f GB\n", ulFileSize.QuadPart, (float)ulFileSize.QuadPart / 1024, (float)ulFileSize.QuadPart / (1024 * 1024), (float)ulFileSize.QuadPart / (1024 * 1024 * 1024));
// ⑥ 撤消映射
// ⑦ 关闭句柄
UnmapViewOfFile(pFile);
CloseHandle(hFileMapping);
CloseHandle(hFile);
system("pause");
return 0;
}
测试结果:申请1M,但实际测试只有64KB。
测试写入时也不用考虑文件尾,因为1M大小就是文件尾。像是之前写的读写文件的测试代码(https://www.cnblogs.com/renleiguanchashi/p/16910946.html),如果写入超过文件尾,会写不进去,要先设置文件尾更大。这里因为文件尾是1M处,很大了,不需要再设置文件尾了。