// 选择文件 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("创建文件对象失败,错误代码:%d/r/n", GetLastError()); return; } // 创建文件映射对象 HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); if (hFileMap == NULL) { TRACE("创建文件映射对象失败,错误代码:%d/r/n", 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("映射文件映射失败,错误代码:%d/r/n", GetLastError()); return; } // 对映射的视图进行访问 for(DWORD i = 0; i < dwBlockBytes; i++) BYTE temp = *(lpbMapAddress + i); // 撤消文件映像 UnmapViewOfFile(lpbMapAddress); // 修正参数 qwFileOffset += dwBlockBytes; qwFileSize -= dwBlockBytes; } // 关闭文件映射对象句柄 CloseHandle(hFileMap); AfxMessageBox("成功完成对文件的访问"); } |
下面分别对这些关键函数进行说明:
1)CreateFile():CreateFile()函数是一个用途非常广泛的函数, 在这里的用法并没有什么特殊的地方,但有几点需要注意:一是访问模式参数dwDesiredAccess。该参数设置了对文件内核对象的访问类型,其允许设置的权限可以为读权限GENERIC_READ、写权限GENERIC_WRITE、读写权限GENERIC_READ | GENERIC_WRITE和设备查询权限0。在使用映射文件时,只能打开那些具有可读访问权限的文件,即只能应用GENERIC_READ和GENERIC_READ | GENERIC_WRITE这两种组合;另一点需要注意的是共享模式参数dwShareMode。该参数定义了对文件内核对象的共享方式,其可能的设置为FILE_SHARE_READ、FILE_SHARE_WRITE和0,并可对其组合使用。其中,设置为0时不允许共享对象;FILE_SHARE_READ和FILE_SHARE_WRITE分别为在要求只读、只写访问的情况下才允许对象的共享。
由于通过内存映射文件可以在多个进程间共享数据,因此在进行这种应用时应当考虑dwShareMode参数设置对运行结果的影响。
2)CreateFileMapping():该函数的作用是创建一个文件映射内核对象,以告知系统文件映射对象需要多大的物理存储器。创建内存映射文件对象对系统资源几乎没有什么影响,也不会影响进程的虚拟地址空间。除了需要用来表示该对象的内部资源之外通常并不用为其分配虚拟内存,但是如果内存映射文件对象是作共享内存之用的话,就要在创建对象时由系统为内存映射文件的使用在系统页文件中保留足够的空间。
函数第一个参数hFile为标识要映射到进程的地址空间的文件的句柄。虽然由于内存映射文件的物理存储器是来自于磁盘上的文件,而非系统的页文件,使创建内存映射文件就像保留一个地址空间区域并将物理存储器提交给该区域一样。第二个参数为指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,由此来决定子进程能否继承得到返回的句柄。通常为其传递NULL值,以默认的安全属性来禁止返回句柄的被继承。
接下来的参数用于文件被映射后设定文件映像的保护属性。其可能的取值为PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY。虽然在创建文件映射对象时,系统并不为其保留地址空间区域,也不将文件的存储器映射到该区域。但是,在系统将存储器映射到进程的地址空间中去时,系统必须确切知道应赋予物理存储器页面的保护属性。在设置保护属性时,必须与用CreateFile()函数打开文件时所指定的访问标识相匹配,否则将导致CreateFileMapping()的执行失败。因此这里设置PAGE_READWRITE属性。除了上述三个页面保护属性外,还有4个区(Section)保护属性也可以一起组合使用:
区保护属性 | 说明 |
SEC_COMMIT | 为区中的所有页面在内存中或磁盘页面文件中分配物理存储器 |
SEC_IMAGE | 告知系统,映射的文件是一个可移植的EXE文件映像 |
SEC_NOCACHE | 告知系统,未将文件的任何内存映射文件放入高速缓存,多供硬件设备驱动程序开发人员使用 |
SEC_RESERVE | 对一个区的所有页面进行保留而不分配物理存储器 |
3)MapViewOfFile():当创建了一个内存映射文件对象并得到其有效句柄后,该句柄即可用来在进程的虚拟地址空间中映射文件的一个映像。在内存映射文件对象已经存在的情况下,映像可被任意映射或取消映射。在文件映像被映射时,仍然必须由系统来为文件的数据保留一个地址空间区域,并将文件的数据作为映射到该区域的物理存储器进行提交。在进程的地址空间中,一个足够大的连续地址空间(通常足以覆盖整个文件映像)将被指定给此文件映像。尽管如此,内存的物理页面还是根据在实际使用中的需求而进行分配的。真正分配一个对应于内存映射文件映像页面的物理内存页面是在发生该页的缺页中断时进行的,这将在第一次读写内存页面中的任一地址时自动完成。MapViewOfFile()即负责映射内存映射文件的一个映像,
函数的第一个参数为CreateFileMapping()所返回的内存映射文件对象句柄,第二个参数指定了对文件映像的访问类型,可能取值有FILE_MAP_WRITE、FILE_MAP_READ、FILE_MAP_ALL_ACCESS和FILE_MAP_COPY等几种,具体的设置要根据文件映射对象允许的保护模式而定。根据前面代码的设置,这里应该使用FILE_MAP_ALL_ACCESS参数。这种机制为对象的创建者提供了对映射此对象的方式进行控制的能力。接下来的2个参数分别指定了内存映射文件的64位偏移地址的低32位和高32位地址,该地址是从内存映射文件头位置到映像开始位置的距离。最后的参数指定了视图的大小,如果设置为0,前面的偏移地址将被忽略,系统将会把整个文件映射为一个映像。MapViewOfFile()如果成功执行,将返回一个指向文件映像在进程的地址空间中的起始地址的指针。如果失败,则返回NULL。在进程中,可以为同一个文件映射对象创建多个文件映像,这些映像可以在系统中共存和重叠,也可以与对应的文件映射对象大小不相一致,但不能大于文件映射对象的大小。
4)UnmapViewOfFile():当不再需要保留映射到进程地址空间区域中的文件映像数据时,可通过调用UnmapViewOfFile()函数将其释放。该函数结构非常简单,只需要提供映像在进程中的起始地址(区域的基地址)作为参数即可。该函数的输入参数为调用MapViewOfFile()时所返回的指向文件映像在进程的地址空间中的起始地址的指针。在调用MapViewOfFile()后,必须确保在进程退出之前能够执行UnmapViewOfFile()函数,否则在进程终止之后先前保留的区域将得不到释放,即使再次启动进程重复调用MapViewOfFile()系统也总是在进程的地址空间中保留一个新的区域,而此前保留的所有区域将得不到释放。
一种比较特殊的情况是,对同一个内存映射文件映射了两个相同的映像的撤消。前面曾经提到过,对于同一个内存映射文件可以有多个映像,这些映像也可以重叠,因此这种情况的存在是合法的。对于这种情况,虽然从表面看上去在单进程的地址空间内是不可能存在两个基地址完全相同的映像的,这将导致无法对这它们的区分。但是事实上,由MapViewOfFile()所返回得到的基地址只是文件映像在进程地址空间中的起始基地址,因此在映射同一内存映射文件的两个相同映像时将会产生对内存映射文件同一部分的两个不同基地址的相同映像,可以用同样的方法调用UnmapViewOfFile()将其从进程的地址空间中予以撤消。
5)CloseHandle(): 与Win32的大多数对象一样,在使用完毕之后总是要通过CloseHandle()函数将已打开的内核对象关闭。如果忘记关闭对象,在程序继续运行时将会出现资源泄漏。虽然在程序退出运行时,操作系统会自动关闭在进程中已经打开但未关闭的任何对象。但是在进程的运行过程中,势必会积累过多的资源句柄。因此在不再需要使用对象的时候通过CloseHandle()将其予以关闭是有意义的。
小结
本文对内存映射文件在大文件处理中的应用作了较为详细的阐述。经实际测试,内存映射文件在处理大数据量文件时表现出了良好的性能,比通常使用CFile类和ReadFile()和WriteFile()等函数的文件处理方式具有明显的优势。本文所述程序代码在Windows 2000 Professional下由Microsoft Visual C++ 6.0编译通过。