共享内存映射2-理论

21 篇文章 2 订阅
20 篇文章 0 订阅
对于一些文件I/O密集型的应用,是否有一种区别于应用本身管理一片内存缓冲池这种相对非常复杂的程序结构的方法,而是直接利用操作系统提供文件—内存管理函数来有效地满足这类应用?

Win32中内存映射文件的引入,将使以上的两个问题得到较好的解决,更使基于Win32编程的开发员们信心大增。

本文将针对基于Win32编程中内存映射文件和对象的使用和管理进行如下几个方面的详细介绍:

1. Windows操作系统中的内存管理;

2. 什么是内存映射文件?

3. 内存映射文件的实现原理;

4. 用内存映射文件来实现内存共享;

5. 内存映射文件函数的使用。

一、Windows操作系统中的内存管理

微软的第一个Windows操作系统版本引入了一种新的动态内存管理方式,它是基于一个所有的应用程序和操作系统本身均共享的全局堆,以及多个私有局部堆,即每个应用程序有各自的局部堆。同时还提供了局部和全局的内存管理函数,这样就为这种新的内存管理系统提供了扩展性。而且最近,Microsoft CRT(C Run-Time)函数库已经加入了在Windows中用原有CRT函数诸如malloc和free来管理这些堆的功能。这样,摆在开发者面前的一个选择是:要么学习作为Window3.1操作系统的一部分而提供新的应用编程接口,要么坚持使用原有的可移植且较为熟悉的CRT函数来为Windows3.1的应用程序编写内存管理部分的程序代码。

这样在加上Win32 API,开发者将会拥有更多的选择。Win32还另外提供了三组在应用程序中管理内存的函数:内存映射文件函数,堆内存函数和虚拟内存函数。这些新的函数并不取代Windows3.1中已有的内存管理函数,相反,他们提供的新的功能将使开发者在编写Win32应用程序的内存管理部分时更加轻松。

图1

如图1所示,Win32中总共有六组内存管理函数,每一组都被设计成独立于其它任何一组。那么,您应该使用哪一组函数呢?对于这一个问题的回答将在很大程度上取决于以下两个方面的考虑:您想使用何种类型的内存管理以及与此相关的函数在操作系统中是如何实现的。也就是说,您是否在创建一个大型的数据库应用程序,在其中需要对大型内存结构的子集进行操作,还是只进行一些简单的动态内存结构的处理,诸如链表和B树,对于这两种情况,您都需要知道那些函数提供的功能与您的要求最为适合以及使用该函数所能达到的资源的准确命中率。

表1对Win32中内存管理函数集进行了如下分类

 

表1. Win32中的不同内存管理函数

二、什么是内存映射文件?

内存映射文件提供了一个统一的内存管理特征,使得应用程序能够通过内存指针象访问动态内存一样对磁盘上的文件进行访问。通过内存映射文件,您可以将磁盘上文件的全部和部分映射为一个视图到进程虚拟地址空间的某个位置。一旦完成了映射视图,对文件内容 的访问就如同在该地址区域内直接对指针取值一样简单。这样,向文件中写入数据就可以直接对指针进行赋值,如:

*pMem = 23;

同样,从文件的某个特定位置读取数据也一样非常的简单:

nTokenLen = *pMem;

在这个例子中,指针pMem 代表映射到文件视图的进程地址空间的某个区域中任何一个地址。事实上,当内存映射文件提供了对文件某个特定位置的直接读写时,真正的对磁盘文件的读写是由底层处理的。而且,数据也并非在每次操作时都即时写入到磁盘,而是大量的文件I/ O通过缓冲处理来提高系统的整体性能。当然,您可以通过调用内存映射文件函数FlushViewOfFile强制立即执行磁盘事务处理以重载这种缓冲处理方式。

内存映射文件给开发者提供了那些优势?

使用内存映射文件I/O的一个好处是系统对所有的数据的传输都是通过4K的数据页面来实现的。内部的所有内存页面都是有虚拟内存管理器来管理的,它决定内存的页面何时被分页对应到磁盘,哪些页面应被释放以提供给其它的应用程序使用,以及每个应用程序可以拥有超出实际分配的物理内存之外多少个页面空间。因为虚拟内存管理器以统一的方式处理所有磁盘I/O——一次以一个页面为单位读写内存,这种优化使它能够以足够快的速度处理内存操作。限制所有的磁盘的读写操作以4K大小的页面为单位进行,意味着一些小的I/O操作将被缓冲入一次大的操作之中,也就是说一次I/O操作以后的其它操作所需的数据已经被前一次的页面操作读入到内存,从而无需再进行一次磁盘I/O,这样将大幅度地减少硬盘读写头的移动,从而提高了系统的性能。一次以一个页面读写内存有时也称为内存分页。

使用内存映射文件的另一个好处是所有的实际I/O交互现在都发生在RAM中并以标准的内存地址形式来访问。同时,磁盘的周期性分页发生在后台由操作系统本身实现,对应用程序是完全透明的。虽然用内存映射文件如果只是将文件读入内存在性能上没有什么提高,但以后的磁盘事务操作将会从中极大地获益。例如,对按记录结构组织的数据库文件,它可能有成上千条连续的记录,访问该文件中的某条记录也只是定位该记录在文件中的位置,并将其读入内存结构。每一次的更新,记录都必须写入文件以保存更新后的信息。这时,对于大记录,有时候一次只读取该记录所需处理的一部分是很有必要的。但是,这种情况又将导致读取记录其它部分时又需进行一次文件操作。而内存映射文件实现这种工作与上述的方式是不同的,当该记录第一次被访问时,包括该记录的整一个4K的页面一次被读入到内存,后续的对该记录的访问只要在此4k页面内,都将是对内存中该页面的直接操作进行处理,而不再需要另外的磁盘I/O操作,直到该文件被关闭或强制刷新。

在系统的正常的分页操作过程中,内存映射文件将被定期地更新。如果系统需要使用已被某个内存映射文件占用的页面时,它可以释放该页面以供其它应用程序使用。若该页面在此时为脏页面,将数据写入磁盘的操作将会自动由系统完成。

三、内存映射文件的实现原理

Windows NT实现的是页式虚拟存储管理,内存映射文件是该内部已有的内存管理组件的一个扩充。在Windows NT中,所有的应用程序在任一给定时刻都可以由磁盘上的一个或多个文件以及驻留在内存中的这些文件的一个子集完全表示。例如,每个应用程序有一个执行文件由执行代码页面和应用程序资源页面组成,这些页面根据需要由操作系统交换进内存或换出内存。当内存中的某个页面不再需要时,操作系统将取消原拥用该页面的应用程序对它的控制权,并将释放该页面以供其它应用程序使用。当该页面再次成为需求页面时,它将被从磁盘上的执行文件中重新读入内存。这种情况称为将内存备份到文件。同样地,当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态和动态数据,一旦对它们的操作被提交后,这些页面也将被备份至系统页面文件,与执行文件被用来备份执行代码页面是非常相似的。图2显示代码页面和数据页面是如何备份在硬盘上的。

 

图2

如果能以一种相同的方式来处理代码和数据页面,将会为在推广该功能以使应用程序能使用该功能铺平道路——这正是Win32通过内存映射文件所要做的。

四、Windows NT中用内存映射文件实现内存共享

Windows NT以相同的方式处理程序代码和数据——都是以内存页面来表示,这些页面在磁盘上都有文件备份。它们之间唯一的真正区别在于被备份的文件不同——程序代码是由执行映象,而数据是由系统页面文件。正是因为如此,内存映射文件可以提供一种机制使得多个进程之间能够共享数据。通过扩展内存映射文件的功能以包括系统页面文件的某个部分,应用程序就可以共享由系统页面文件进行备份的数据。如图3所示,每个应用程序只要简单地对页面文件中的同一个部分映射一个视图,就可以使每个应用程序共享内存中的同一页面。

 

图3

Windows NT的安全机制不允许进程之间进行直接的信息共享,但是内存映射文件提供了一种符合该安全系统的共享机制。为使各个进程能通过内存映射文件实现数据共享,首先每个进程必须对该文件要有共同的访问路径。共同的访问路径可以由给内存映射文件赋一个以供各个进程打开该文件时使用的名字来实现。

在系统内部,页面文件的共享区要转换到内存的页面,这样多个进程均可以寻址到。为实现这种共享,Windows NT使用一个称为原型页表项(PPTE)的内部资源。PPTE使得多个进程能够对内存的同一个物理页面进行寻址,它作为内部资源,它的可用性和安全性均仅由系统自身控制。这样,进程之间既可以共享数据,又可以保证处于一个安全的操作系统之中。图4说明了PPTE在Windows NT的虚存寻址方式中是如何使用的。

 

图4

五、内存映射文件函数的使用

在Win32中,内存映射文件函数可以看作是虚拟内存管理函数的姐妹组函数。它与虚存管理函数一样,都将直接影响到进程的地址空间和物理内存页面,而且在管理文件视图上,除了存在于所有进程中的基本虚存管理之外没有其它的开销。这些函数只处理内存中的保留页面和进程中的确定地址区域。内存映射文件函数包括:

CreateFileMapping

OpenFileMapping

MapViewOfFile

MapViewOfFileEx

UnmapViewOfFile

FlushViewOfFile

CloseHandle

每个函数将配以具体的代码例子进行详细介绍他们的使用方法。

5.1 创建文件映射

在使用内存映射文件之前,首先要创建一个内存映射文件对象。创建内存映射文件对象对系统资源几乎没有什么影响,也不影响进程的地址空间,除了需要用来表示该对象的内部资源之外也无需为该对象分配虚存。有一个例外的是,如果内存映射文件对象代表的是共享内存,那么在创建该对象时系统必须要为内存映射文件的使用在系统页面文件中保留足够的空间。

函数CreateFileMapping用来创建内存映射文件对象,如下一段代码例子显示了该函数的使用方法:

case IDM_MMFCREATENEW:

{

char szTmpFile[256];

 

GetTempPath (256, szTmpFile);

GetTempFileName (szTmpFile,

"PW",

0,

MMFiles[wParam-IDM_MMFCREATE].szMMFile);

 

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;

 

*szFilePath = 0;

if (!GetFileName (hWnd, szFilePath, "*"))

break;

 

if ((MMFiles[wParam-IDM_MMFCREATE].hFile =

OpenFile (szFilePath, &of, OF_READWRITE)) !=

HFILE_ERROR)

goto MAP_FILE;

}

break;

case IDM_MMFCREATE:

 

MMFiles[wParam-IDM_MMFCREATE].hFile = (HANDLE)0xffffffff;

MAP_FILE:

 

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;

上例显示了创建内存映射文件的三种情况:创建内存映射文件的之前先建立一个临时的磁盘文件;在已存在的文件之上创建内存映射文件;在系统页面文件中创建一个内存映射文件。在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 打开一个内存映射文件对象

若要打开某个文件映射对象,该对象必须是以有名对象创建的。该对象名是本进程和其它进程共享此对象的唯一标识,以下代码段说明了如何用对象名来打开一个文件映射对象,

 

LoadString (hDLL, IDS_MAPFILENAME, szMapFileName, MAX_PATH);

 

if ((hMMFile = OpenFileMapping (FILE_MAP_WRITE,

FALSE,

szMapFileName)))

 

return TRUE;

 

if (!GetIniFile (hDLL, szIniFilePath))

return FALSE;

 

if ((int)(hFile = (HANDLE)OpenFile (szIniFilePath,

&of,

OF_READ)) == -1)

return FALSE;

else

{

nFileSize = GetFileSize (hFile, NULL);

CloseHandle (hFile);

}

 

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函数来映射内存映射文件的一个视图,这两个函数均要求的一个参数是内存映射文件对象的句柄。下例是该函数的使用方法;

 

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撤销其映射。该函数相对非常的简单,只要提供此视图在进程中的起始地址作为参数。

 

LoadString (hDLL, IDS_PORTAPIS, szSection, MAX_PATH);

if (!LoadSection (szIniFilePath,

szSection,

PT_APIS,

&nOffset,

lpMMFile))

{

 

UnmapViewOfFile (lpMMFile);

CloseHandle (hMMFile);

return FALSE;

}

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

5.5 刷新文件视图

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

 

FlushViewOfFile (lpMMFile, nMMFileSize);

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

5.6 释放内存映射文件

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

 

CloseHandle (hMMFile);

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

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值