一直都对内存映射文件这个概念很模糊,不知道它和虚拟内存有什么区别,而且映射这个词也很让人迷茫,今天终于搞清楚了。。。下面,我先解释一下我对映射这个词的理解,再区分一下几个容易混淆的概念,之后,什么是内存映射就很明朗了。
首先,“映射”这个词,就和数学课上说的“一一映射”是一个意思,就是建立一种一一对应关系,在这里主要是只 硬盘上文件 的位置与进程 逻辑地址空间 中一块大小相同的区域之间的一一对应,如图1中过程1所示。这种对应关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在的。在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。
图1.内存映射原理
既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程了。
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,如图1中过程2所示。这个过程与内存映射无关。
前面讲过,建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如图1中过程3所示。这个过程与内存映射无关。
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如图1中过程4所示。这个过程也与内存映射无关。
从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。但是通过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么呢?原因是read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,如图2中过程1,然后再将这些数据拷贝到用户空间,如图2中过程2,在这个过程中,实际上完成了 两次数据拷贝 ;而mmap()也是系统调用,如前所述,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了 一次数据拷贝 。因此,内存映射的效率要比read/write效率高。
图2.read系统调用原理
下面这个程序,通过read和mmap两种方法分别对硬盘上一个名为“mmap_test”的文件进行操作,文件中存有10000个整数,程序两次使用不同的方法将它们读出,加1,再写回硬盘。通过对比可以看出,read消耗的时间将近是mmap的两到三倍。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/time.h>
#include<fcntl.h>
#include<sys/mman.h>
#define MAX 10000
int main()
{
int i=0;
int count=0, fd=0;
struct timeval tv1, tv2;
int *array = (int *)malloc( sizeof(int)*MAX );
/*read*/
gettimeofday( &tv1, NULL );
fd = open( "mmap_test", O_RDWR );
if( sizeof(int)*MAX != read( fd, (void *)array, sizeof(int)*MAX ) )
{
printf( "Reading data failed.../n" );
return -1;
}
for( i=0; i<MAX; ++i )
++array[ i ];
if( sizeof(int)*MAX != write( fd, (void *)array, sizeof(int)*MAX ) )
{
printf( "Writing data failed.../n" );
return -1;
}
free( array );
close( fd );
gettimeofday( &tv2, NULL );
printf( "Time of read/write: %dms/n", tv2.tv_usec-tv1.tv_usec );
/*mmap*/
gettimeofday( &tv1, NULL );
fd = open( "mmap_test", O_RDWR );
array = mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
for( i=0; i<MAX; ++i )
++array[ i ];
munmap( array, sizeof(int)*MAX );
msync( array, sizeof(int)*MAX, MS_SYNC );
free( array );
close( fd );
gettimeofday( &tv2, NULL );
printf( "Time of mmap: %dms/n", tv2.tv_usec-tv1.tv_usec );
return 0;
}
输出结果:
Time of read/write: 154ms
Time of mmap: 68ms
Windows提供了3种进行内存管理的方法:
• 虚拟内存,最适合用来管理大型对象或结构数组。
• 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
• 内存堆栈,最适合用来管理大量的小对象。
内存映射文件
• 系统使用内存映射文件,以便加载和执行. exe和DLL文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。
• 可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。
• 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。
若要使用内存映射文件,必须执行下列操作步骤:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。
2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。
3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。
2) 关闭文件映射内核对象。
3) 关闭文件内核对象。
下面将详细介绍这些操作步骤。
步骤1:创建或打开文件内核对象
HANDLE CreateFile(
PCSTR pszFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
dwDesiredAccess的值
值 |
含义 |
0 |
不能读取或写入文件的内容。当只想获得文件的属性时,请设定0 |
GENERIC_READ |
可以从文件中读取数据 |
GENERIC_WRITE |
可以将数据写入文件 |
GENERIC_READ |GENERIC_WRITE |
可以从文件中读取数据,也可以将数据写入文件 |
dwShareMode 的值
值 |
含义 |
0 |
打开文件的任何尝试均将失败 |
FILE_SHARE_READ |
使用GENERIC_WRITE打开文件的其他尝试将会失败 |
FILE_SHARE_WRITE |
使用GENERIC_READ打开文件的其他尝试将会失败 |
FILE_SHARE_READ FILE_SHARE_WRITE| |
打开文件的其他尝试将会取得成功 |
步骤2:创建一个文件映射内核对象
调用CreateFileMapping函数告诉系统,文件映射对象需要多少物理存储器。
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD fdwProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
第一个参数:hFile用于标识你想要映射到进程地址空间中的文件句柄。该句柄由前面调用的CreateFile函数返回。
第二个参数:psa参数是指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,通常传递的值是NULL(它提供默认的安全特性,返回的句柄是不能继承的)。
第三个参数:fdwProtect参数使你能够设定这些保护属性。大多数情况下,可以设定下表列出的3个保护属性之一。
使用fdwProtect 参数设定的部分保护属性
保护属性 |
含义 |
PAGE_READONLY |
当文件映射对象被映射时,可以读取文件的数据。必须已经将GENERIC_READ传递给CreateFile函数 |
PAGE_READWRITE |
当文件映射对象被映射时,可以读取和写入文件的数据。必须已经将GENERIC_READ | GENERIC_WRITE传递给Creat eFile |
PAGE_WRITECOPY |
当文件映射对象被映射时,可以读取和写入文件的数据。如果写入数据,会导致页面的私有拷贝得以创建。必须已经将GENERIC_READ或GENERIC_WRITE传递给CreateFile |
除了上面的页面保护属性外,还有4个节保护属性
节的第一个保护属性是SEC_NOCACHE,它告诉系统,没有将文件的任何内存映射页面放入高速缓存。因此,当将数据写入该文件时,系统将更加经常地更新磁盘上的文件数据。供设备驱动程序开发人员使用的,应用程序通常不使用。
节的第二个保护属性是SEC_IMAGE,它告诉系统,你映射的文件是个可移植的可执行(PE)文件映像。当系统将该文件映射到你的进程的地址空间中时,系统要查看文件的内容,以确定将哪些保护属性赋予文件映像的各个页面。例如, PE文件的代码节( . text)通常用PAGE_ EXECUTE_READ属性进行映射, 而PE 文件的数据节( .data) 则通常用PAGE_READW RITE属性进行映射。如果设定的属性是S E C _ I M A G E,则告诉系统进行文件映像的映射,并设置相应的页面保护属性。
最后两个保护属性是SEC_RESERVE和SEC_COMMIT,它们是两个互斥属性。只有当创建由系统的页文件支持的文件映射对象时,这两个标志才有意义。SEC_COMMIT标志能使CreateFileMapping从系统的页文件中提交存储器。如果两个标志都不设定,其结果也一样。
第四和五个参数:dwMaximumSizeHigh和dwMaximumSizeLow这两个参数将告诉系统该文件的最大字节数
最后一个参数是pszName: 它是个以0结尾的字符串,用于给该文件映射对象赋予一个名字。该名字用于与其他进程共享文件映射对象。
步骤3:将文件数据映射到进程的地址空间
将文件的数据作为映射到该区域的物理存储器进行提交。
PVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap);
第一个参数: hFileMappingObject用于标识文件映射对象的句柄,该句柄是前面调用CreateFileMapping或OpenFileMapping函数返回的。
第二个参数:dwDesiredAccess用于标识如何访问该数据。可以设定下表所列的4个值中的一个。
值 |
含义 |
FILE_MAP_WRITE |
可以读取和写入文件数据。CreateFileMapping函数必须通过传递PAGE_READWRITE标志来调用 |
FILE_MAP_READ |
可以读取文件数据。CreateFileMapping函数可以通过传递下列任何一个保护属性来调用:PAGE_READONLY、PAGE_ READWRITE或PAGE_WRITECOPY |
FILE_MAP_ALL_ACCES S |
与FILE_MAP_WRITE相同 |
FILE_MAP_COPY |
可以读取和写入文件数据。如果写入文件数据,可以创建一个页面的私有拷贝。在Windows 2000中,CreateileMapping函数可以用PAGE_READONLY、PAGE_READWRITE或PAGE_WRITECOPY等保护属性中的任何一个来调用。在Windows 98中,CreateFileMapping必须用PAGE_WRITECOPY来调用 |
(一个文件映射到你的进程的地址空间中时,你不必一次性地映射整个文件。相反,可以只将文件的一小部分映射到地址空间。被映射到进程的地址空间的这部分文件称为一个视图。)
第三四个参数:dwFileOfsetHigh和dwFileOfsetLow参数。指定哪个字节应该作为视图中的第一个字节来映射。
第五个参数:dwNumberOfBytesToMap有多少字节要映射到地址空间。如果设定的值是0,那么系统将设法把从文件中的指定位移开始到整个文件的结尾的视图映射到地址空间。
步骤4:从进程的地址空间中撤消文件数据的映像
当不再需要保留映射到进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:
BOOL UnmapViewOfFile(PVOID pvBaseAddress);
参数:pvBaseAddress由MapViewOfFile函数返回。
注意:如果没有调用这个函数,那么在进程终止运行前,保留的区域就不会被释放。每当调用MapViewOfFile时,系统总是在你的进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。
为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用FlushViewOfFile函数:
BOOL FlushViewOfFile(
PVOID pvAddress,
SIZE_T dwNumberOfBytesToFlush);
第一个参数是包含在内存映射文件中的视图的一个字节的地址。该函数将你在这里传递的地址圆整为一个页面边界值。
第二个参数用于指明你想要刷新的字节数。系统将把这个数字向上圆整,使得字节总数是页面的整数。如果你调用FlushViewOfFile函数并且不修改任何数据,那么该函数只是返回,而不将任何信息写入磁盘。
步骤5和步骤6:关闭文件映射对象和文件对象
用CloseHandle函数关闭相应的对象。
在代码开始运行时关闭这些对象:
HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
CloseHandle(hFile);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);
CloseHandle(hFileMapping);
// Use the memory-mapped file.
UnmapViewOfFile(pvFile);
// ------------------------------------------------------------
// 文件名 : 17_FileMapping2.cpp
// 创建者 : 方煜宽
// 邮箱 : [email protected]
// 创建时间 : 2010-7-12 23:50
// 功能描述 : 内存映射数据文件
//
// ------------------------------------------------------------
#include "stdafx.h"
#include "windows.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
// Open the file that we want to map.
// 注意请在c盘,自己创建一个kuan.txt文件,并写入内容
HANDLE hFile = ::CreateFile(L"C:\\kuan.txt",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
// Create a file-mapping object for the file.
HANDLE hFileMapping = ::CreateFileMapping(hFile,
NULL,
PAGE_WRITECOPY,
0, 0,
NULL);
PBYTE pbFile = (PBYTE)::MapViewOfFile(hFileMapping, FILE_MAP_COPY, 0, 0, 0);
cout << pbFile << endl;
::UnmapViewOfFile(pbFile);
::CloseHandle(hFileMapping);
::CloseHandle(hFile);
return 0;
}
本文地址:http://www.cnblogs.com/fangyukuan/archive/2010/09/09/1822216.html