基于内存映射原理的高速海量数据采集与存储技术
cutedeer(内附我的代码)
内存映射文件技术是windows操作系统提供的一种新的文件数据存取机制,利用内存映射文件技术,系统可以在2GB的地址空间中为文件保留一部分空间,并将文件映射到这块保留空间,一旦文件被映射后,操作系统将管理页映射、缓冲以及高速缓冲等任务,而不需要调用分配、释放内存块和文件输入/输出的API函数,也不需要自己提供任何缓冲算法。
当遇到大数据量文件时,内存映射文件技术能够为我们分配一块足够大的内存来满足请求。这一显著特点是与操作系统的内存管理有密切关系的。在win32操作系统中,每个win32进程都拥有自己的地址空间,虽然它们可以具有相同值的指针来进行寻址,但是为了保证进程之间的相互独立性,一个进程不能存取另一个进程的私有数据,这样就使系统的健壮性得到了增强;另一方面,每个win32进程都用用4GB的地址空间,但这4GB的地址空间仅仅是虚拟地址而不是真正的实际物理内存。一般情况下,只有在需要的时候,才会有操作系统为之提交,在不同情况下,系统提交的物理内存的类型是不同的,可能是RAM,也可能是硬盘模拟的虚拟内存。总之,正是由于操作系统的内存管理,内存映射文件技术才得以实现。
Windows操作系统对win32进程地址空间的划分如下:
内存映射文件分三种情况,第一种是可执行文件的内存映射,主要由操作系统自身使用;第二种是数据文件的内存映射;第三种是借助于页面交换文件的内存映射。系统在进行工作时,首先把数据文件的一部分映射到虚拟地址空间(映射的区域是0x8000~0xBFFF)内,但不提交RAM,存取这段内存指令时会产生一个页面异常,系统捕获到这个异常后,分配一页RAM,并把它映射到当前进程发生异常的地址处,然后系统把文件中相应的数据读到这个页面中,继续执行刚才产生异常的指令。这就是应用程序自己不需要调用I/O函数的原因,也是内存映射文件技术的工作机理。
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32 API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件
内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
内存映射文件并不是简单的文件I/O操作,实际用到了Windows的核心编程技术--内存管理。所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识,内存管理的相关知识非常复杂,超出了本文的讨论范畴,在此就不再赘述,感兴趣的读者可以参阅其他相关书籍。下面给出使用内存映射文件的一般方法:
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
内存映射文件相关函数
在使用内存映射文件时,所使用的API函数主要就是前面提到过的那几个函数,下面分别对其进行介绍:
HANDLE CreateFile(LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
函数CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对象,并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccess和dwShareMode,错误的参数设置将会导致相应操作时的失败。
HANDLE CreateFileMapping(HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName);
CreateFileMapping()函数创建一个文件映射内核对象,通过参数hFile指定待映射到进程地址空间的文件句柄(该句柄由CreateFile()函数的返回值获取)。由于内存映射文件的物理存储器实际是存储于磁盘上的一个文件,而不是从系统的页文件中分配的内存,所以系统不会主动为其保留地址空间区域,也不会自动将文件的存储空间映射到该区域,为了让系统能够确定对页面采取何种保护属性,需要通过参数flProtect来设定,保护属性PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY分别表示文件映射对象被映射后,可以读取、读写文件数据。在使用PAGE_READONLY时,必须确保CreateFile()采用的是GENERIC_READ参数;PAGE_READWRITE则要求CreateFile()采用的是GENERIC_READ|GENERIC_WRITE参数;至于属性PAGE_WRITECOPY则只需要确保CreateFile()采用了GENERIC_READ和GENERIC_WRITE其中之一即可。DWORD型的参数dwMaximumSizeHigh和dwMaximumSizeLow也是相当重要的,指定了文件的最大字节数,由于这两个参数共64位,因此所支持的最大文件长度为16EB,几乎可以满足任何大数据量文件处理场合的要求。
LPVOID MapViewOfFile(HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
MapViewOfFile()函数负责把文件数据映射到进程的地址空间,参数hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。虽然这里一再对保护属性进行重复设置看似多余,但却可以使应用程序能更多的对数据的保护属性实行有效控制。MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;
参数dwNumberOfBytesToMap指定了数据文件的映射长度,这里需要特别指出的是,对于Windows 9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);但是在Windows 2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放,该函数原型声明如下:
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。
除了前面这些必须的API函数之外,在使用内存映射文件时还要根据情况来选用其他一些辅助函数。例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件的磁盘映像。为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。
BOOL FlushViewOfFile(LPVOID lpBaseAddress, DWORD dwNumberOfBytesToFlush);
该函数需要调用MapViewOfFile()所返回的被映射的视图地址以及写磁盘的字节数。如果调用FlushViewOfFile并且没有数据被改动过,此函数仅返回,不向磁盘写任何数据。
以下附上我自己使用的程序代码:
1.创建映射文件
//创建规定大小的内存映射文件
m_hFile = CreateFile(m_FileName, GENERIC_READ |GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(m_hFile == INVALID_HANDLE_VALUE)
{
AfxMessageBox("创建内存映射文件失败!");
return;
}
//创建文件映射对象
m_hMap = CreateFileMapping(m_hFile, NULL, PAGE_READWRITE, 0, 文件大小(单位M)*1024*1024, NULL);
if (m_hMap == NULL)
{
AfxMessageBox("创建文件映射对象失败!");
return;
}
CloseHandle(m_hFile);
//首先取得系统分配粒度
SYSTEM_INFO SysInfo;
GetSystemInfo(&SysInfo);
dwGran = SysInfo.dwAllocationGranularity;
//得到被处理文件长度(64位)的高32位和低32位值
DWORD dwFileSizeHigh;
qwFileSize = GetFileSize(m_hFile, &dwFileSizeHigh);
qwFileSize |= (((__int64)dwFileSizeHigh) << 32);
qwFileOffset=0;
qwFileAlarm=600*dwGran;
dwBytesInBlock=1000*dwGran;
DWORD dwBlockBytes = 文件大小(单位M)*1024*1024;//1000 * dwGran;
//映射视图
lpbMapAddress = (PDWORD)MapViewOfFile(m_hMap,FILE_MAP_ALL_ACCESS, (DWORD)(qwFileOffset >> 32), (DWORD)(qwFileOffset & 0xFFFFFFFF),dwBlockBytes);
2.映射数据
在你的采集线程内利用偏移地址来往映射内存中写入数据:
*(lpbMapAddress+qwFileOffset)=data;
//内存映射地址自动累加
qwFileOffset++;
3.保存数据
采集结束后退出线程,需要保存数据
m_hFileSave=CreateFile(m_FileName(保存文件), GENERIC_READ |GENERIC_WRITE, 0, NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
//以实际数据长度创建另外一个文件映射内核对象
m_hMapSave = CreateFileMapping(m_hFileSave, NULL, PAGE_READWRITE, 0, ((DWORD)qwFileOffset&0xffffffff)*sizeof(DWORD));, NULL);
//关闭文件内核对象
CloseHandle(m_hFileSave);
//将文件数据映射到进程的地址空间
lpbMapAddressSave=(PDWORD)MapViewOfFile(m_hMapSave,FILE_MAP_ALL_ACCESS, 0, 0,qwFileOffset*sizeof(DWORD));
//将数据从原来的内存映射文件复制到此内存映射文件
memcpy(lpbMapAddressSave,lpbMapAddress,qwFileOffset*sizeof(DWORD)););
//从进程的地址空间撤销文件数据映象
UnmapViewOfFile(lpbMapAddress);
UnmapViewOfFile(lpbMapAddressSave);
//关闭文件映射对象
CloseHandle(m_hMap);
CloseHandle(m_hMapSave);
//删除临时文件
DeleteFile(m_FileName);
4.注意事项
注意保存数据的类型,即往内存里映射的数据类型和最后存储的数据类型一定要对应,否则会丢失大量数据。
__int64 qwFileSize; //内存映射文件大小
__int64 qwFileOffset; //内存映射文件数据存储偏移量
-THE END