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处,很大了,不需要再设置文件尾了。

 

posted @ 2022-11-21 11:59  人类观察者  阅读(56)  评论(0编辑  收藏  举报