Bootstrap

C++使用内存映射读写大文件

由于公司项目要求,组件A每毫秒会产生五百万条数据,需要进行存储,使用IO流速度过于缓慢,于是决定采用内存映射的方法进行存储,效率提高了许多,于是对查询的资料,以及写代码过程中遇到的一些问题进行整理。

对于内存映射,需要用到几个关键的Windows 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);

创建一个文件映射内核对象,通过参数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;

在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放以及文件的关闭:
 

BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);

唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。

具体实现如下:

#include <iostream>
#include <atlstr.h> 
#include <Windows.h>
#include <WinBase.h>
using namespace std;
 
int main()
{
    const char* shared_name = "test";
    const char* file_name = "C:\\1.txt";
    const DWORD mmf_size = 512*1024;
    //存取模式
    DWORD access_mode = (GENERIC_READ|GENERIC_WRITE);
    //共享模式
    DWORD share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE;
    //文件属性
    DWORD flags = FILE_FLAG_SEQUENTIAL_SCAN;//|FILE_FLAG_WRITE_THROUGH|FILE_FLAG_NO_BUFFERING;
    DWORD error_code;
     
    //格式转换
	//const char * 转 LPCWSTR
    char* szStr = "Mikasoi";  
	CString str = CString(file_name);
	USES_CONVERSION;
	LPCWSTR wszClassName = A2CW(W2A(str));
	str.ReleaseBuffer();
	
    //创建文件
    HANDLE mmHandle =
        CreateFile(wszClassName,
             access_mode, 
             share_mode, 
             NULL, 
             OPEN_ALWAYS,
             flags,
             NULL);
 
    if (mmHandle == INVALID_HANDLE_VALUE) 
	{
        error_code = GetLastError();
        cout<<"创建mmf失败:"<<error_code<<endl;
        return -1; 
    }
    
    DWORD high_size;
    DWORD file_size = GetFileSize(mmHandle, &high_size);
    if (file_size == 0xFFFFFFFF && (error_code = GetLastError()) != 0) 
	{
        CloseHandle(mmHandle);            
        cout<<"error:"<<error_code<<endl;
        return -2;
    }
    cout<<"create mmf sucessfully"<<endl;

    DWORD size_high = 0;
    //创建文件映射,如果要创建内存页面文件的映射,第一个参数设置为INVALID_HANDLE_VALUE
    HANDLE mmfm = CreateFileMapping(mmHandle,
        NULL,
        PAGE_READWRITE,
        size_high, 
        mmf_size, 
        shared_name);

    error_code = GetLastError();
    if(0 != error_code)
	{
        cout<<"createFileMapping error"<<error_code<<endl;
        return -3;
    }
    if(mmfm == NULL){
        if(mmHandle != INVALID_HANDLE_VALUE){
            CloseHandle(mmHandle);
        }
    }

    size_t view_size = 1024*256;
    DWORD view_access = FILE_MAP_ALL_ACCESS;

    //获得映射视图
    char* mmfm_base_address = (char*)MapViewOfFile(mmfm,view_access,0,0,view_size);
    if(mmfm_base_address == NULL)
	{
        error_code = GetLastError();
        if(error_code != 0)
		{
            cout<<"error code "<<error_code<<endl;
            return -4;
        }
    }
	else
	{
        char write_chars[] = "hello chars";
        const size_t write_chars_size = sizeof(write_chars);
        
        //向内存映射视图中写数据
        memcpy(mmfm_base_address,write_chars,write_chars_size);
         
        size_t position = 0;
        char read_chars[write_chars_size];

        //读数据
        memcpy(read_chars,mmfm_base_address,write_chars_size);
        cout<<"read chars "<<read_chars<<endl;
         
        //卸载映射
        UnmapViewOfFile(mmfm_base_address);
        //关闭内存映射文件
        CloseHandle(mmfm);
        //关闭文件
        CloseHandle(mmHandle);
    } 
 
    system("pause");

    return EXIT_SUCCESS;
}

这个代码应该是没有问题的,不过是在公司写好的,在家里写的博客,可能记忆有些偏差,不过应该也不会出现编译错误。

 

附:参考文章:

https://blog.csdn.net/mikasoi/article/details/81347854

https://www.cnblogs.com/yukaizhao/archive/2011/05/18/MapViewOfFile_CreateFileMapping.html

;