若要使用内存映射文件,必须执行下列操作步骤:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。
2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。
3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
4) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。
5) 关闭文件映射内核对象。
6) 关闭文件内核对象。
若要创建或打开一个文件内核对象,总是要调用CreateFile 函数:
HANDLE CreateFile(
LPCTSTR lpFileName , // file name
DWORD dwDesiredAccess , // access mode
DWORD
dwShareMode
,
// share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes , // SD
DWORD dwCreationDisposition , // how to create
DWORD dwFlagsAndAttributes , // file attributes
HANDLE hTemplateFile // handle to template file
);
第一个参数pszFileName 用于指明要创建或打开的文件的名字(包括一个选项路径)。
第二个参数dwDesiredAccess 用于设定如何访问该文件的内容。
dwDesiredAccess 的值 | 值含义 |
0 | 不能读取或写入文件的内容。当只想获得文件的属性时,请设定0 |
G E N E R I C _ R E A D | 可以从文件中读取数据 |
G E N E R I C _ W R I T E | 可以将数据写入文件 |
GENERIC_READ |GENERIC_WRITE | 可以从文件中读取数据,也可以将数据写入文件 |
当创建或打开一个文件,将它作为一个内存映射文件来使用时,请选定最有意义的一个或
多个访问标志,以说明你打算如何访问文件的数据。对内存映射文件来说,必须打开用于只读访问或读写访问的文件。
第三个参数dwShareMode 告诉系统你想如何共享该文件。
dwShareMode 的值 | 值含义 |
0 | 打开文件的任何尝试均将失败 |
FILE_SHARE_READ | 使用GENERIC_WRITE 打开文件的其他尝试将会失败 |
GENERIC_WRITE | 使用FILE_SHARE_READ 打开文件的其他尝试将会失败 |
FILE_SHARE_READ | FILE_SHARE_WRITE | 打开文件的其他尝试将会取得成功 |
如果CreateFile 函数成功地创建或打开指定的文件,便返回一个文件内核对象的句柄,否则返回INVLID_HANDLE_VALUE 。
[ 注意] 能够返回句柄的大多数Windows 函数如果运行失败,那么就会返回NULL 。但是,CreateFile 函数将返回INVLID_HANDLE_VALUE ,它定义为((HANDLE )- 1 )。
调用CreateFile 函数,就可以将文件映像的物理存储器的位置告诉操作系统。你传递的路径
名用于指明支持文件映像的物理存储器在磁盘(或网络或光盘)上的确切位置。这时,必须告诉系统,文件映射对象需要多少物理存储器。若要进行这项操作,可以调用CreateFileMapping 函数:
HANDLE CreateFileMapping(
HANDLE hFile , // handle to file
LPSECURITY_ATTRIBUTES lpAttributes , // security
DWORD flProtect , // protection
DWORD dwMaximumSizeHigh , // high-order DWORD of size
DWORD
dwMaximumSizeLow
,
// low-order DWORD of size
LPCTSTR lpName // object name
);
第一个参数hFile 用于标识你想要映射到进程地址空间中的文件句柄。该句柄由前面调用的
CreateFile 函数返回。psa 参数是指向文件映射内核对象的SECURITY_ATTRIBUTES 结构的指针,通常传递的值是NULL 它提供默认的安全特性,返回的句柄是不能继承的)。
创建内存映射文件就像保留一个地址空间区域然后将物理存储器提交给该区域一样。因为内存映射文件的物理存储器来自磁盘上的一个文件,而不是来自从系统的页文件中分配的空间。当创建一个文件映射对象时,系统并不为它保留地址空间区域,也不将文件的存储器映射到该区域。但是,当系统将存储器映射到进程的地址空间中去时,系统必须知道应该将什么保护属性赋予物理存储器的页面。CreateFileMapping 函数的fdwProtect 参数使你能够设定这些保护属性。
fdwProtect | 保护属性含义 |
PAGE_READONLY | 当文件映射对象被映射时,可以读取文件的数据。必须已经将GENERIC_READ 传递给CreateFile 函数 |
PAGE_READWRITE | 当文件映射对象被映射时,可以读取和写入文件的数据。必须已经将GENERIC_READ | GENERIC_WRITE 传递给CreateFile |
PAGE_WRITECOPY | 当文件映射对象被映射时,可以读取和写入文件的数据。 如果写入数据,会导致页面的私有拷贝得以创建。必须已经将GENERIC_READ 或GENERIC_WRITE 传递给CreateFile |
除了上面的页面保护属性外,还有4 个节保护属性,可以用OR 将它们连接起来放入CreateFileMapping 函数的fdwProtect 参数中。节只是用于内存映射的另一个术语。节的第一个保护属性是SEC_NOCACHE ,它告诉系统,没有将文件的任何内存映射页面放入高速缓存。因此,当将数据写入该文件时,系统将更加经常地更新磁盘上的文件数据。这个标志与PAGE_NOCACHE 保护属性标志一样,是供设备驱动程序开发人员使用的,应用程序通常不使用。
节的第二个保护属性是SEC_IMAGE ,它告诉系统,你映射的文件是个可移植的可执行(P E )文件映像。当系统将该文件映射到你的进程的地址空间中时,系统要查看文件的内容,以确定将哪些保护属性赋予文件映像的各个页面。
最后两个保护属性是SEC_RESERVE 和SEC_COMMIT ,它们是两个互斥属性,当使用内存映射数据文件时,它们不能使用。当创建内存映射数据文件时,不应该设定这些标志中的任何一个标志。CreateFileMapping 将忽略这些标志。
CreateFileMapping 的另外两个参数是dwMaximumSizeHigh 和dwMaximumSizeLow ,它们是两个最重要的参数。CreateFileMapping 函数的主要作用是保证文件映射对象能够得到足够的物
理存储器。这两个参数将告诉系统该文件的最大字节数。它需要两个3 2 位的值,因为Windows 支持的文件大小可以用6 4 位的值来表示。dwMaximumSizeHigh 参数用于设定较高的3 2 位,而dwMaximumSizeLow 参数则用于设定较低的32 位值。对于4GB 或小于4GB 的文件来说,dwMaximumSizeHigh 的值将始终是0 。
当创建了一个文件映射对象后,仍然必须让系统为文件的数据保留一个地址空间区域,并将文件的数据作为映射到该区域的物理存储器进行提交。可以通过调用MapViewOfFile 函数来进行这项操作:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject , // handle to file-mapping object
DWORD dwDesiredAccess , // access mode
DWORD dwFileOffsetHigh , // high-order DWORD of offset
DWORD dwFileOffsetLow , // low-order DWORD of offset
SIZE_T dwNumberOfBytesToMap // number of bytes to map
);
参数hFileMappingObject 用于标识文件映射对象的句柄,该句柄是调用CreateFile Mapping 或OpenFileMapping 函数返回的。
参数dwDesiredAccess 用于标识如何访问该数据。
dwDesiredAccess | 值含义 |
FILE_MAP_WRITE | 可以读取和写入文件数据。CreateFileMapping 函数必须通过传递PAGE_READWRITE 标志来调用 |
FILE_MAP_READ | 可以读取文件数据。CreateFileMapping 函数可以通过传递下列任何一个保护属性来调用:PAGE_READONLY 、PAGE_READWRITE 或PAGE_WRITECOPY |
FILE_MAP_ALL_ACCESS | 与FILE_MAP_WRITE 相同 |
FILE_MAP_COPY | 可以读取和写入文件数据。如果写入文件数据,可以创建一个页面的私有拷贝。在Windows 2000 中,CreateFileMapping 函数可以用PAGE_READONLY 、PAGE_READWRITE 或PAGE_WRITECOPY 等保护属性中的任何一个来调用。 |
如果在调用MapViewOfFile 函数时设定了FILE_MAP_COPY 标志,系统就会从系统的页文件中提交物理存储器。提交的地址空间数量由dwNumberOfBytesToMap 参数决定。只要你不进行其他操作,只是从文件的映像视图中读取数据,那么系统将决不会使用页文件中的这些提交的页面。但是,如果进程中的任何线程将数据写入文件的映像视图中的任何内存地址,那么系
统将从页文件中抓取已提交页面中的一个页面,将原始数据页面拷贝到该页交换文件中,然后将该拷贝的页面映射到你的进程的地址空间。从这时起,你的进程中的线程就要访问数据的本地拷贝,不能读取或修改原始数据。当系统制作原始页面的拷贝时,系统将把页面的保护属性从PAGE_WRITECOPY 改为PAGE_READWRITE.
当不再需要保留映射到你的进程地址空间区域中的文件数据时,可以通过调用下面的函数
将它释放:
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress // starting address
);
该函数的唯一的参数pvBaseAddress 用于设定返回区域的基地址。该值必须与调用MapViewOfFile 函数返回的值相同。必须记住要调用UnmapViewOfFile 函数。如果没有调用这
个函数,那么在你的进程终止运行前,保留的区域就不会被释放。每当你调用MapViewOfFile 时,系统总是在你的进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。
为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用FlushViewOfFile 函数:
BOOL FlushViewOfFile(
LPCVOID lpBaseAddress , // starting address
SIZE_T dwNumberOfBytesToFlush // number of bytes in range
);
第一个参数是包含在内存映射文件中的视图的一个字节的地址。该函数将你在这里传递的地址圆整为一个页面边界值。
第二个参数用于指明你想要刷新的字节数。系统将把这个数字向上圆整,使得字节总数是页面的整数。如果你调用FlushViewOfFile 函数并且不修改任何数据,那么该函数只是返回,而不将任何信息写入磁盘。
对于存储器是在网络上的内存映射文件来说,FlushViewOfFile 能够保证文件的数据已经从工作站写入存储器。但是FlushViewOfFile 不能保证正在共享文件的服务器已经将数据写入远程磁盘,因为服务器也许对文件的数据进行了高速缓存。若要保证服务器写入文件的数据,每当你为文件创建一个文件映射对象并且映射该文件映射对象的视图时,应该将FILE_FLAG_WRITE_THROUGH 标志传递给CreateFile 函数。如果你使用该标志打开该文件,那么只有当文件的全部数据已经存放在服务器的磁盘驱动器中的时候, FlushViewOfFile 函数才返回。
记住UnmapViewOfFile 函数的一个特殊的特性。如果原先使用FILE_MAP_COPY 标志来映射视图,那么你对文件的数据所作的任何修改,实际上是对存放在系统的页文件中的文件数据的拷贝所作的修改。在这种情况下,如果调用UnmapViewOfFile 函数,该函数在磁盘文件上就没有什么可以更新,而只会释放页文件中的页面,从而导致数据丢失。如果想保留修改后的数据,必须采用别的措施。例如,你可以用同一个文件创建另一个文件映射对象(使用PAGE_READWRITE ),然后使用FILE_MAP_WRITE 标志将这个新文件映射对象映射到进程的地址空间。之后,你可以扫描第一个视图,寻找带有PAGE_READWRITE 保护属性的页面。每当你找到一个带有该属性的页面时,可以查看它的内容,并且确定是否将修改了的数据写入该文件。如果不想用新数据更新该文件,那么继续对视图中的剩余页面进行扫描,直到视图的结尾。但是,如果你确实想要保存修改了的数据页面,那么只需要调用MoveMemory 函数,将数据页面从第一个视图拷贝到第二个视图。由于第二个视图是用PAGE_READWRITE 保护属性映射的,因此MoveMemory 函数将更新磁盘上的实际文件内容。可以使用这种方法来确定文件的变更并保存你的文件的数据。
不用说,你总是要关闭你打开了的内核对象。如果忘记关闭,在你的进程继续运行时会出现资源泄漏的问题。当然,当你的进程终止运行时,系统会自动关闭你的进程已经打开但是忘记关闭的任何对象。但是如果你的进程暂时没有终止运行,你将会积累许多资源句柄。因此你始终都应该编写清楚而又“正确的”代码,以便关闭你已经打开的任何对象。若要关闭文件映射对象和文件对象,只需要两次调用CloseHandle 函数,每个句柄调用一次.
系统允许你映射一个文件的相同数据的多个视图。例如,你可以将文件开头的10KB 映射到一个视图,然后将同一个文件的头4 KB 映射到另一个视图。只要你是映射相同的文件映射对象,系统就会确保映射的视图数据的相关性。例如,如果你的应用程序改变了一个视图中的文件内容,那么所有其他视图均被更新以反映这个变化。这是因为尽管页面多次被映射到进程
的虚拟地址空间,但是系统只将数据放在单个RAM 页面上。如果多个进程映射单个数据文件的视图,那么数据仍然是相关的,因为在数据文件中,每个RAM 页面只有一个实例——正是这个R A M 页面被映射到多个进程的地址空间。
然而,当对文件进行操作时,没有理由使另一个应用程序无法调用CreateFile 函数以打开由另一个进程映射的同一个文件。这个新进程可以使用ReadFile 和WriteFile 函数来读取该文件的数据和将数据写入该文件。当然,每当一个进程调用这些函数时,它必须从内存缓冲区读取文件数据或者将文件数据写入内存缓冲区。该内存缓冲区必须是进程自己创建的一个缓冲区,而不是映射文件使用的内存缓冲区。当两个应用程序打开同一个文件时,问题就可能产生:一个进程可以调用ReadFile 函数来读取文件的一个部分,并修改它的数据,然后使用WriteFile 函数将数据重新写入文件,而第二个进程的文件映射对象却不知道第一个进程执行的这些操作。由于这个原因,当你为将被内存映射的文件调用CreateFile 函数时,最好将dwShareMode 参数的值设置为0 。这样就可以告诉系统,你想要单独访问这个文件,而其他进程都不能打开它。
只读文件不存在相关性问题,因此它们可以作为很好的内存映射文件。内存映射文件决不应该用于共享网络上的可写入文件,因为系统无法保证数据视图的相关性。如果某个人的计算机更新了文件的内容,其他内存中含有原始数据的计算机将不知道它的信息已经被修改。
可以创建由系统的页文件支持的内存映射文件,而不是由专用硬盘文件支持的内存映射文件。这个方法与创建内存映射磁盘文件所用的方法几乎相同,不同之处是它更加方便。一方面,它不必调用CreateFile 函数,因为你不是要创建或打开一个指定的文件,你只需要像通常那样调用CreateFileMapping 函数,并且传递INVALID_HANDLE_VALUE 作为hFile 参数。这将告诉系统,你不是创建其物理存储器驻留在磁盘上的文件中的文件映射对象,相反,你想让系统从它的页文件中提交物理存储器。分配的存储器的数量由CreateFileMapping 函数的dwMaximumSizeHigh 和dwMaximumSizeLow 两个参数来决定。
当创建了文件映射对象并且将它的一个视图映射到进程的地址空间之后,就可以像使用任
何内存区域那样使用它。如果你想要与其他进程共享该数据,可调用CreateFileMapping 函数,并传递一个以0 结尾的字符串作为pszName 参数。然后,想要访问该存储器的其他进程就可以调用CreateFileMapping 或OpenFileMapping 函数,并传递相同的名字。当进程不再想要访问文件映射对象时,该进程应该调用CloseHandle 函数。当所有句柄均被关闭后,系统将从系统的页文件中收回已经提交的存储器。
Program 1:
int _tmain2 (int argc , _TCHAR * argv [])
{
HANDLE hFile = CreateFile ( _T ("shareMemory.dat" ), GENERIC_WRITE |GENERIC_READ , FILE_SHARE_WRITE |FILE_SHARE_READ , NULL , OPEN_ALWAYS , NULL , NULL );
if ( hFile != INVALID_HANDLE_VALUE )
{
HANDLE hFileMap = CreateFileMapping ( hFile , NULL , PAGE_READWRITE , 0, 4*1024, NULL );
if ( hFileMap )
{
LPTSTR pStr = (LPTSTR )MapViewOfFile ( hFileMap , FILE_MAP_WRITE , 0, 0, 0 );
HANDLE hWait = CreateEvent ( NULL , FALSE , FALSE , _T ("SHARE_MEMORY_EVENT" ) );
HANDLE hLock = CreateMutex ( NULL , FALSE , _T ("SHARE_MEMORY_MUTEX" ) );
while (TRUE )
{
WaitForSingleObject ( hLock , INFINITE );
scanf ( "%s" , pStr );
ReleaseMutex (hLock );
SetEvent (hWait );
Sleep (0);
}
UnmapViewOfFile (pStr );
CloseHandle (hFileMap );
}
CloseHandle (hFile );
}
return 0;
}
Program 2:
int _tmain2 (int argc , _TCHAR * argv [])
{
HANDLE hFile = CreateFile ( _T ("shareMemory.dat" ), GENERIC_WRITE |GENERIC_READ , FILE_SHARE_WRITE |FILE_SHARE_READ , NULL , OPEN_ALWAYS , NULL , NULL );
if ( hFile != INVALID_HANDLE_VALUE )
{
HANDLE hFileMap = CreateFileMapping ( hFile , NULL , PAGE_READWRITE , 0, 4*1024, NULL );
if ( hFileMap )
{
LPTSTR pStr = (LPTSTR )MapViewOfFile ( hFileMap , FILE_MAP_READ , 0, 0, 0 );
HANDLE hWait = CreateEvent ( NULL , FALSE , FALSE , _T ("SHARE_MEMORY_EVENT" ) );
HANDLE hLock = CreateMutex ( NULL , FALSE , _T ("SHARE_MEMORY_MUTEX" ) );
ReleaseMutex (hLock );
while (TRUE )
{
WaitForSingleObject ( hWait , INFINITE );
WaitForSingleObject ( hLock , INFINITE );
printf ( "%s/n" , pStr );
ReleaseMutex (hLock );
}
UnmapViewOfFile (pStr );
CloseHandle (hFileMap );
}
CloseHandle (hFile );
}
return 0;
}
Program 1:
int _tmain (int argc , _TCHAR * argv [])
{
HANDLE hFileMap = CreateFileMapping ( INVALID_HANDLE_VALUE , NULL , PAGE_READWRITE , 0, 4*1024, "SHARE_MEMORY_FILEMAP" );
if ( hFileMap )
{
LPTSTR pStr = (LPTSTR )MapViewOfFile ( hFileMap , FILE_MAP_WRITE , 0, 0, 0 );
// ……
UnmapViewOfFile (pStr );
CloseHandle (hFileMap );
}
return 0;
}
Program 2:
int _tmain (int argc , _TCHAR * argv [])
{
HANDLE hFileMap = CreateFileMapping ( INVALID_HANDLE_VALUE , NULL , PAGE_READWRITE , 0, 4*1024, "SHARE_MEMORY_FILEMAP" );
if ( hFileMap )
{
LPTSTR pStr = (LPTSTR )MapViewOfFile ( hFileMap , FILE_MAP_READ , 0, 0, 0 );
// ……
UnmapViewOfFile (pStr );
CloseHandle (hFileMap );
}
return 0;
}