zoukankan      html  css  js  c++  java
  • 内存映射文件(专门读写大文件)


    引言

      文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。

      内存映射文件概述

      内存文件映射也是Windows的一种内存管理方法,提供了一个统一的内存管理特征,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问。通过文件映射这种使磁盘文件的全部或部分内容与进程虚拟地址空间的某个区域建立映射关联的能力,可以直接对被映射的文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。

      在使用内存映射文件进行I/O处理时,系统对数据的传输按页面来进行。至于内部的所有内存页面则是由虚拟内存管理器来负责管理,由其来决定内存页面何时被分页到磁盘,哪些页面应该被释放以便为其它进程提供空闲空间,以及每个进程可以拥有超出实际分配物理内存之外的多少个页面空间等等。由于虚拟内存管理器是以一种统一的方式来处理所有磁盘I/O的(以页面为单位对内存数据进行读写),因此这种优化使其有能力以足够快的速度来处理内存操作。

      使用内存映射文件时所进行的任何实际I/O交互都是在内存中进行并以标准的内存地址形式来访问。磁盘的周期性分页也是由操作系统在后台隐蔽实现的,对应用程序而言是完全透明的。内存映射文件的这种特性在进行大文件的磁盘事务操作时将获得很高的效益。

      需要说明的是,在系统的正常的分页操作过程中,内存映射文件并非一成不变的,它将被定期更新。如果系统要使用的页面目前正被某个内存映射文件所占用,系统将释放此页面,如果页面数据尚未保存,系统将在释放页面之前自动完成页面数据到磁盘的写入。

      对于使用页虚拟存储管理的Windows操作系统,内存映射文件是其内部已有的内存管理组件的一个扩充。由可执行代码页面和数据页面组成的应用程序可根据需要由操作系统来将这些页面换进或换出内存。如果内存中的某个页面不再需要,操作系统将撤消此页面原拥用者对它的控制权,并释放该页面以供其它进程使用。只有在该页面再次成为需求页面时,才会从磁盘上的可执行文件重新读入内存。同样地,当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态、动态数据,一旦对它们的操作被提交,这些页面也将被备份至系统的页面文件,这与可执行文件被用来备份执行代码页面的过程是很类似的。图1展示了代码页面和数据页面在磁盘存储器上的备份过程:

    图1 进程的代码页、数据页在磁盘存储器上的备份

      显然,如果可以采取同一种方式来处理代码和数据页面,无疑将会提高程序的执行效率,而内存映射文件的使用恰恰可以满足此需求。

    // 选择文件
    CFileDialog fileDlg(TRUE, "*.txt", "*.txt", NULL, "文本文件 (*.txt)|*.txt||", this);
    fileDlg.m_ofn.Flags |= OFN_FILEMUSTEXIST;
    fileDlg.m_ofn.lpstrTitle = "通过内存映射文件读取数据";
    if (fileDlg.DoModal() == IDOK)
    {
     // 创建文件对象
     HANDLE hFile = CreateFile(fileDlg.GetPathName(), GENERIC_READ | GENERIC_WRITE,
       0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
     if (hFile == INVALID_HANDLE_VALUE)
     {
      TRACE("创建文件对象失败,错误代码:%drn", GetLastError());
      return;
     }
     // 创建文件映射对象
     HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
     if (hFileMap == NULL)
     {
      TRACE("创建文件映射对象失败,错误代码:%drn", GetLastError());
      return;
     }
     // 得到系统分配粒度
     SYSTEM_INFO SysInfo;
     GetSystemInfo(&SysInfo);
     DWORD dwGran = SysInfo.dwAllocationGranularity;
     // 得到文件尺寸
     DWORD dwFileSizeHigh;
     __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
     qwFileSize |= (((__int64)dwFileSizeHigh) << 32);
     // 关闭文件对象
     CloseHandle(hFile);
     // 偏移地址 
     __int64 qwFileOffset = 0;
     // 块大小
     DWORD dwBlockBytes = 1000 * dwGran;
     if (qwFileSize < 1000 * dwGran)
      dwBlockBytes = (DWORD)qwFileSize;
      while (qwFileOffset > 0)
      {
       // 映射视图
       LPBYTE lpbMapAddress = (LPBYTE)MapViewOfFile(hFileMap,FILE_MAP_ALL_ACCESS, 
          (DWORD)(qwFileOffset >> 32), (DWORD)(qwFileOffset & 0xFFFFFFFF),
          dwBlockBytes);
       if (lpbMapAddress == NULL)
       {
        TRACE("映射文件映射失败,错误代码:%drn", GetLastError());
        return;
       }
       // 对映射的视图进行访问
       for(DWORD i = 0; i < dwBlockBytes; i++)
        BYTE temp = *(lpbMapAddress + i);
        // 撤消文件映像
        UnmapViewOfFile(lpbMapAddress);
        // 修正参数
        qwFileOffset += dwBlockBytes;
        qwFileSize -= dwBlockBytes;
      }
      // 关闭文件映射对象句柄
      CloseHandle(hFileMap);
      AfxMessageBox("成功完成对文件的访问");
    }


      在本例中,首先通过GetFileSize()得到被处理文件长度(64位)的高32位和低32位值。然后在映射过程中设定每次映射的块大小为 1000倍的分配粒度,如果文件长度小于1000倍的分配粒度时则将块大小设置为文件的实际长度。在处理过程中由映射、访问、撤消映射构成了一个循环处理。其中,每处理完一个文件块后都通过关闭文件映射对象来对每个文件块进行整理。CreateFileMapping()、 MapViewOfFile()等函数是专门用来进行内存文件映射处理用的。

    区保护属性 说明 SEC_COMMIT 为区中的所有页面在内存中或磁盘页面文件中分配物理存储器 SEC_IMAGE 告知系统,映射的文件是一个可移植的EXE文件映像 SEC_NOCACHE 告知系统,未将文件的任何内存映射文件放入高速缓存,多供硬件设备驱动程序开发人员使用 SEC_RESERVE 对一个区的所有页面进行保留而不分配物理存储器


      后面的两个参数指定了要创建的文件映射对象的最大字节数的高32位值和低32位值,实际也就设定了文件的最大字节数(最大可以处理16EB的文件)。这两个参数可以满足确保文件映射对象能够得到足够的物理存储器这一基本条件。在参数设置的大小小于文件实际大小时,系统将从文件映射指定的字节数。这里将其设置为0,将使所创建的文件映射对象将为文件的当前大小,以上两种情况均无法改变文件的大小。如果设置的参数大于文件的实际大小,系统将会在 CreateFileMapping()函数返回前扩展该文件。需要指出的是,文件映射对象的大小是静态的,一旦创建完毕后将无法更改。如果设置的文件映射对象尺寸偏小将导致无法对文件进行全面的访问。

      在本节开始也曾提到过,创建文件映射对象是不需要花费什么系统资源的,因此遵循"宁多勿缺"的原则,一般应将文件映射对象的大小设置为文件大小的相同值。函数最后的参数将可以为映射对象命名。如果想打开一个已存在的文件映射对象,该对象必须要命名。对该名字字符串的要求仅限于未被其它对象使用过的名字即可。

      CreateFileMapping()在成功执行后将返回一个指向文件映射对象的句柄。如果对一个已经存在的文件映射对象调用了 CreateFileMapping()函数,进程将得到一个指向现有映射对象的句柄。通过调用GetLastError()可以得到返回值 ERROR_ALREADY_EXIST,由此可以判断当前得到的内存映射对象句柄是新创建的还是打开已经存在的。如果系统无法创建文件映射对象,将导致 CreateFileMapping()的执行失败,返回N U L L句柄值。


  • 相关阅读:
    Java实现 蓝桥杯VIP 基础练习 完美的代价
    Java实现 蓝桥杯VIP基础练习 矩形面积交
    Java实现 蓝桥杯VIP 基础练习 完美的代价
    Java实现 蓝桥杯 蓝桥杯VIP 基础练习 数的读法
    Java实现 蓝桥杯 蓝桥杯VIP 基础练习 数的读法
    Java实现 蓝桥杯 蓝桥杯VIP 基础练习 数的读法
    Java实现 蓝桥杯 蓝桥杯VIP 基础练习 数的读法
    Java实现 蓝桥杯 蓝桥杯VIP 基础练习 数的读法
    核心思想:想清楚自己创业的目的(如果你没有自信提供一种更好的产品或服务,那就别做了,比如IM 电商 搜索)
    在Linux中如何利用backtrace信息解决问题
  • 原文地址:https://www.cnblogs.com/med-dandelion/p/4532316.html
Copyright © 2011-2022 走看看