如何在com server(dll)里正确使用文件映射对象,特别是如何正确的释放。(200分)

  • 主题发起人 主题发起人 g622
  • 开始时间 开始时间
g633问的问题果然有难度
Windows NT实现的是页式虚拟存储管理,内存映射文件是该内部已有的内存管理组件的一
个扩充。在Windows NT中,所有的应用程序在任一给定时刻都可以由磁盘上的一个或多个
文件以及驻留在内存中的这些文件的一个子集完全表示。例如,每个应用程序有一个执行
文件由执行代码页面和应用程序资源页面组成,这些页面根据需要由操作系统交换进内存
或换出内存。当内存中的某个页面不再需要时,操作系统将取消原拥用该页面的应用程序
对它的控制权,并将释放该页面以供其它应用程序使用。当该页面再次成为需求页面时,
它将被从磁盘上的执行文件中重新读入内存。这种情况称为将内存备份到文件。同样地,
当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态和动态数据,一旦对
它们的操作被提交后,这些页面也将被备份至系统页面文件,与执行文件被用来备份执行
代码页面是非常相似的。
在Win32中,内存映射文件函数可以看作是虚拟内存管理函数的姐妹组函数。它与虚存管理
函数一样,都将直接影响到进程的地址空间和物理内存页面,而且在管理文件视图上,除了
存在于所有进程中的基本虚存管理之外没有其它的开销。这些函数只处理内存中的保留页面
和进程中的确定地址区域。内存映射文件函数包括:
CreateFileMapping
OpenFileMapping
MapViewOfFile
MapViewOfFileEx
UnmapViewOfFile
FlushViewOfFile
CloseHandle
5.1 创建文件映射
在使用内存映射文件之前,首先要创建一个内存映射文件对象。创建内存映射文件对象对系统资源几乎没有什么影响,也不影响进程的地址空间,除了需要用来表示该对象的内部资源之外也无需为该对象分配虚存。有一个例外的是,如果内存映射文件对象代表的是共享内存,那么在创建该对象时系统必须要为内存映射文件的使用在系统页面文件中保留足够的空间。
函数CreateFileMapping用来创建内存映射文件对象,如下一段代码例子显示了该函数的使用方法:

case IDM_MMFCREATENEW:
{
char szTmpFile[256];

/* Create temporary file for mapping. */
GetTempPath (256, szTmpFile);
GetTempFileName (szTmpFile,
"PW",
0,
MMFiles[wParam-IDM_MMFCREATE].szMMFile);

/* If file created, continue to map file. */
if ((MMFiles[wParam-IDM_MMFCREATE].hFile =
CreateFile (MMFiles[wParam-IDM_MMFCREATE].szMMFile,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY,
NULL)) != (HANDLE)INVALID_HANDLE_VALUE)
goto MAP_FILE;
}
break;

case IDM_MMFCREATEEXIST:
{
char szFilePath[MAX_PATH];
OFSTRUCT of;

/* Get existing filename for mapfile. */
*szFilePath = 0;
if (!GetFileName (hWnd, szFilePath, "*"))
break;

/* If file opened, continue to map file. */
if ((MMFiles[wParam-IDM_MMFCREATE].hFile =
(HANDLE)OpenFile (szFilePath, &of, OF_READWRITE)) !=
(HANDLE)HFILE_ERROR)
goto MAP_FILE;
}
break;

case IDM_MMFCREATE:
/* Associate shared memory file handle value. */
MMFiles[wParam-IDM_MMFCREATE].hFile = (HANDLE)0xffffffff;

MAP_FILE:
/* Create 20MB file mapping. */
if (!(MMFiles[wParam-IDM_MMFCREATE].hMMFile =
CreateFileMapping (MMFiles[wParam-IDM_MMFCREATE].hFile,
NULL,
PAGE_READWRITE,
0,
0x01400000,
NULL)))
{
ReportError (hWnd);
if (MMFiles[wParam-IDM_MMFCREATE].hFile)
{
CloseHandle (MMFiles[wParam-IDM_MMFCREATE].hFile);
MMFiles[wParam-IDM_MMFCREATE].hFile = NULL;
}
}
break; /* from IDM_MMFCREATE */



上例显示了创建内存映射文件的三种情况:创建内存映射文件的之前先建立一个临时的磁盘
文件;在已存在的文件之上创建内存映射文件;在系统页面文件中创建一个内存映射文件。
在IDM_MMFCREATENEW这种情况下,创建内存映射文件之前先创建了一个临时文件;
IDM_MMFCREATEEXIST情况下,用打开文件对话框先获取一个文件名,打开该文件,然后创
建内存映射文件;IDM_MMFCREATE的情况,创建内存映射文件要么使用系统页面文件或者是
以上两种情况下的标准文件。

CreateFileMapping函数的第一个参数hFile,用来制定准备作内存映射的文件句柄,如果
使用的是系统页面文件,则hFile的值必须置为0xFFFFFFFF。 在上例中,是用一个结构来
存放标准文件和内存映射文件信息,结构MMFiles[wParam-IDM_MMFCREATE]中成员hMMFile
的值或为缺省的0xFFFFFFFF,或者是其它两种情况下的文件句柄。

在以上的三种情况中,内存映射文件的大小均为20M,而不管创建的或打开的还是映射的
文件本身的大小。第四个参数dwMaximumSizeHigh 和第五个 dwMaximumSizeLow将指定文
件映射的大小。如果指定大小的内存映射文件不是使用系统页面文件,则磁盘上的物理文
件将根据这一新的大小进行相应的调整以使两个值相等。另外一种处理方法是,在映射磁
盘上的文件时,将映射大小第四第五个参数置为0,这样得到的内存映射文件与原始磁盘
上的文件大小是一致的。当映射页面文件的某个区域时,必须要指定内存映射文件的大小。

CreateFileMapping的第二个参数lpsa是指向结构SECURITY_ATTRIBUTES的一个指针。由于
内存映射文件是一个对象,那它与其它的所有对象都具有相同的安全属性。如置为NULL,
则不为该内存映射文件指定相关的安全属性。

第三个参数fdwProtect指定作用于该内存映射文件的保护类型,通过设定该参数为
PAGE_READONLY来阻止对内存映射文件的写操作,若置为PAGE_READWRITE则允许对它进行
读写操作。

最后一个参数lpszMapName,可以给内存映射文件指定一个名字。如果想打开一个已存在
的文件映射对象,该对象必须要命名。对该名字字符串的要求仅限于未被其它对象使用
过的名字即可。

5.2 获取文件映射对象的句柄
映射一个内存映射文件的视图,首先要获得该内存映射文件对象的一个有效句柄。可以
通过如下几种方式来获得此句柄:创建文件映射对象,以函数OpenFileMapping打开一个
对象,通过继承一个对象句柄,或是复制一个句柄。

5.2.1 打开一个内存映射文件对象
若要打开某个文件映射对象,该对象必须是以有名对象创建的。该对象名是本进程和其
它进程共享此对象的唯一标识,以下代码段说明了如何用对象名来打开一个文件映射对
象,
/* Load name for file-mapping object. */
LoadString (hDLL, IDS_MAPFILENAME, szMapFileName, MAX_PATH);

/* After first process initializes, port data. */
if ((hMMFile = OpenFileMapping (FILE_MAP_WRITE,
FALSE,
szMapFileName)))
/* Exit now since initialization was already performed by
another process. */
return TRUE;

/* Retrieve path and file for ini file. */
if (!GetIniFile (hDLL, szIniFilePath))
return FALSE;

/* Test for ini file existence and get length of file. */
if ((int)(hFile = (HANDLE)OpenFile (szIniFilePath,
&of,
OF_READ)) == -1)
return FALSE;

else
{
nFileSize = GetFileSize (hFile, NULL);
CloseHandle (hFile);
}

/* Allocate a segment of the swap file for shared memory 2*Size
of ini file. */
if (!(hMMFile = CreateFileMapping ((HANDLE)0xFFFFFFFF,
NULL,
PAGE_READWRITE,
0,
nFileSize * 2,
szMapFileName)))
return FALSE;

函数OpenFileMapping只有三个参数,最重要的就是对象名。如上例所示,对象名只是一个
简单的唯一的字符串,若该字符串在系统中不唯一,则内存映射文件对象将无法创建成功。
一旦一个对象已经存在,则它的对象名在该对象的生命周期内仅与它相对应。

如果在创建内存映射文件对象之前执行OpenFileMapping来打开此对象,将因为对象不存在
而失败。这在DLL中非常有用,因为DLL的初始化部分将被与此相连的进程反复调用。
5.2.2 继承及复制内存映射文件对象的句柄
通常情况下,两个进程要共享内存映射文件,可以通过对象名来确定。一种例外情况是子
进程可以继承父进程的句柄。Win32中的大部分句柄可以显式地设定为可继承和不可继承,
但某些句柄如GDI对象句柄是不可以继承的。创建内存映射文件对象时,
SECURITY_ATTRIBUTES结构中一个布尔成员可以指定该句柄是否可继承。如果内存映射文件
对象被指定为可继承,则创建该对象进程的所有子进程均可通过该句柄访问此对象。

函数DuplicateHandle提供对句柄的更多的控制权诸如句柄何时能够被继承或不可继承,该
函数可以用来创建原始句柄的一个复制句柄并且修改该句柄的继承属性。应用程序在将某
个句柄传给子进程之前调用该函数来改变内存映射文件对象的继承属性为允许子进程继承,
也可以作相反的处理,阻止一个可继承的句柄被子进程继承。

5.3 映射内存映射文件的一个视图
获得内存映射文件对象的有效句柄后,该句柄即可用来在进程的地址空间内映射该文件的
一个视图。在内存映射文件对象已存在的情况下,视图可以任意映射或取消映射。当一个
文件的视图被映射时,系统将为此分配系统资源,在进程地址空间内,一个足够以覆盖文
件视图的连续地址空间将被指定给此文件视图,尽管如此,内存的物理页面还是基于实际
使用中的需求来分配。真正分配一个对应于内存映射文件视图页面的物理内存页面是在发
生该页的缺页中断时进行的,在第一次读写内存页面中任一地址时自动完成的。

使用MapViewOfFile或MapViewOfFileEx函数来映射内存映射文件的一个视图,这两个函数
均要求的一个参数是内存映射文件对象的句柄。下例是该函数的使用方法;

/* Map a view of this file for writing. */
lpMMFile = (char *)MapViewOfFile (hMMFile,
FILE_MAP_WRITE,
0,
0,
0);

本例中,整个文件要被映射,所以最后的三个参数均为0。第一个参数指定内存映射文件对
象,第二个参数指定文件视图的访问模式,取值有如下几种:FILE_MAP_READ,
FILE_MAP_WRITE, FILE_MAP_ALL_ACCESS,根据文件映射对象允许的保护模式。如果该对象
是以PAGE_READWRITE保护模式创建的,则所有的三种访问模式都支持;如果是以
PAGE_READONLY模式创建的,那访问类型只FILE_MAP_READ。这种机制可以保证对象的创建
者对如何映射此对象进行控制。

第三和第四个参数分别用来指定内存映射文件的64位偏移地址低32地址和高32位地址,这
个偏移地址是从内存映射文件头位置到视图开始位置的距离。最后一个参数指定视图的大
小,若该参数置为0,则偏移地址将被忽略,整个文件被映射成一个视图。

该函数返还指向文件视图在进程地址空间中的起始地址的指针。如要指定文件视图映射到
进程地址空间的某个区域,可以使用函数MapViewOfFileEx。此函数增加了一个地址参数
lpvBase,指定视图在进程地址空间中的映射位置,如果执行成功,返还值与lpvBase相等,
相反则为NULL。MapViewOfFile失败时也返还NULL。

图5显示了同一个文件映射对象的多个视图可以在系统中共存及互相重叠。


图5

5.4 撤销内存映射文件的视图映射
一个内存映射文件的视图被映射后,该视图可在时刻通过调用函数UnmapViewOfFile撤销
其映射。该函数相对非常的简单,只要提供此视图在进程中的起始地址作为参数。
/* Load tokens for APIS section. */
LoadString (hDLL, IDS_PORTAPIS, szSection, MAX_PATH);
if (!LoadSection (szIniFilePath,
szSection,
PT_APIS,
&nOffset,
lpMMFile))
{
/* Clean up memory-mapped file. */
UnmapViewOfFile (lpMMFile);
CloseHandle (hMMFile);
return FALSE;
}

上面已经提到,可以对同一个内存映射文件有多个视图,它们也可以重叠。如果对同一个
内存映射文件映射两个相同的视图又将如何呢?看了刚才的撤销视图映射的函数后,你可
能会得出如下的结论:在单一进程地址空间内是不可能有两个完全相同的视图,因为它们
的基地址将会是相同的,从而使你无法区分它们。事实并非如此,函数MapViewOfFile和
MapViewOfFileEx返还的基地址不是文件视图的基地址,而是文件视图在进程地址空间中
的起始基地址。所以,映射同一个内存映射文件的两个相同的视图将产生两个不同基地址
的视图,当然它们是对内存映射文件同一部分的相同视图。

5.5 刷新文件视图
内存映射文件的一个重要特征是可以根据需要将对文件的修改立即写到磁盘上,该功能是
由函数FlushViewOfFile提供的。通过文件的视图对内存映射文件的修改,如不是系统页面
文件,将会在视图撤销映射或文件映射对象被删除时自动写到磁盘上。当然如果应用程序
需要将修改立即写出,就可以通过调用FlushViewOfFile来实现。

/* Force changes to disk immediately. */
FlushViewOfFile (lpMMFile, nMMFileSize);

本例是将整个文件视图刷出到磁盘上,在完成此操作时,系统只是将脏页面写出到磁盘。
由于Windows NT的虚拟存储管理器自动跟踪对页面所作的修改,这样在某个地址区间内
找出所有的脏页面就非常的容易,然后将其写出磁盘。这个地址区间可由表示文件视图
基地址的第一个参数和表示延伸的长度的第二个参数cbFlush确定。唯一的要求是这个范
围要在单个文件视图的边界之内。

5.6 释放内存映射文件
同Win32子系统中大多数对象一样,内存映射文件对象是通过调用CloseHandle将其关闭。
在关闭对象之前没有必要撤销内存映射文件的所有视图。如上所提到的,在对象被释放之
前,所有的脏页面将自动写出到磁盘。关闭内存映射文件,调用CloseHandle函数,内存
映射文件对象的句柄作为其参数。

/* Close memory-mapped file. */
CloseHandle (hMMFile);

关闭内存映射文件只是释放该对象,如果内存映射文件代表的是磁盘上的文件,这个还必
须用标准的文件I/O函数来关闭。

内存映射文件为Win32的应用编程接口提供了一套独特的管理内存的方法。它使得一个应用
程序能够将其虚拟地址空间直接映射到磁盘的文件上,而且一旦文件被内存映射后,访问
它的内容就只是读取指针的值。

一个内存映射文件可以同时被多个应用程序映射。这也是Windows NT中多个进程之间直接
共享数据的唯一机制。通过内存映射文件,进程可以将一个普通的文件和文件的一部分映
射到各自的地址空间的一个唯一的位置,这种技术保证了Windows NT中所有进程的私有地
址空间的完整性。

内存映射文件在操作大文件时也是非常有用的。因为创建内存映射文件消耗很少的物理
资源,所以那种巨大的文件可以由进程通过这种方式打开,它对系统的影响只是微乎其
微。然后,文件的一个很小的部分“视图”在执行I/O之前可以映射到进程的地址空间中
去。

提供给Win32应用程序管理内存的技术非常的多,但不管你是获得内存共享的优势,还只
是想简单管理有磁盘文件支持的虚拟内存,内存映射文件函数都将提供你所需的支持
 
你的回答果然有广度



:)
 

Similar threads

D
回复
0
查看
943
DelphiTeacher的专栏
D
D
回复
0
查看
882
DelphiTeacher的专栏
D
S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
后退
顶部