1、前言
Windows提供了3种进行内存管理的方法:
• 虚拟内存,最适合用来管理大型对象或结构数组。
• 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
• 内存堆栈,最适合用来管理大量的小对象。
内存映射文件可以用于3个不同的目的
• 系统使用内存映射文件,以便加载和执行. exe和DLL文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。
• 可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。
• 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。
2、使用内存映射数据文件
若要使用内存映射文件,必须执行下列操作步骤:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。
2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。
3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。
2) 关闭文件映射内核对象。
3) 关闭文件内核对象。
2.1 创建或打开文件内核对象
HANDLE CreateFile(
PCSTR pszFileName, /*文件路径*/
DWORD dwDesiredAccess, /*请求权限*/
DWORD dwShareMode, /*共享模式*/
PSECURITY_ATTRIBUTES psa, /*安全特性*/
DWORD dwCreationDisposition, /*创建动作*/
DWORD dwFlagsAndAttributes, /*文件属性和标记*/
HANDLE hTemplateFile /*模板文件*/
);
1)pszFileName:指示文件路径名
2)dwDesiredAccess:指示文件权限
值 | 含义 |
0 | 不能读取或写入文件的内容。当只想获得文件的属性时,请设定0 |
GENERIC_READ | 可以从文件中读取数据 |
GENERIC_WRITE | 可以将数据写入文件 |
GENERIC_READ | GENERIC_WRITE | 可以从文件中读取数据,也可以将数据写入文件 |
3)dwShareMode:文件共享模式
值 | 含义 |
0 | 打开文件的任何尝试均将失败 |
FILE_SHARE_READ | 使用GENERIC_WRITE打开文件的其他尝试将会失败 |
FILE_SHARE_WRITE | 使用GENERIC_READ打开文件的其他尝试将会失败 |
FILE_SHARE_READ | FILE_SHARE_WRITE | 打开文件的其他尝试将会取得成功 |
4)psa:指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,通常传递的值是NULL(它提供默认的安全特性,返回的句柄是不能继承的)。
5)dwCreationDisposition:指示当文件存在或不存在时的动作。
值 | 含义 |
CREATE_ALWAYS | 始终创建为新文件,如果已存在将重写文件 |
CREATE_NEW | 仅当文件不存在时创建新文件 |
OPEN_ALWAYS | 总是打开文件,如果不存在将创建一个文件并打开 |
OPEN_EXISTING | 仅当文件存在时,打开文件 |
TRUNCATE_EXISTING | 仅当文件存在时,打开文件并清空数据 |
6)dwFlagsAndAttributes:文件属性,最常用默认值:FILE_ATTRIBUTE_NORMAL (参考MSDN)
7)hTemplateFile:指示模板文件。可为空,当打开已存在文件时,忽略该参数。
2.2 创建一个文件映射内核对象
调用CreateFileMapping函数告诉系统,文件映射对象需要多少物理存储器
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD fdwProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName
);
1)hFile:用于标识你想要映射到进程地址空间中的文件句柄。该句柄由前面调用的CreateFile函数返回。
2)psa:指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,通常传递的值是NULL(它提供默认的安全特性,返回的句柄是不能继承的)。
3)fdwProtect:使你能够设定这些保护属性。大多数情况下,可以设定下表列出的3个保护属性之一:
使用fdwProtect 参数设定的部分保护属性:
保护属性 | 含义 |
PAGE_READONLY | 当文件映射对象被映射时,可以读取文件的数据。必须已经将GENERIC_READ传递给CreateFile函数 |
PAGE_READWRITE | 当文件映射对象被映射时,可以读取和写入文件的数据。必须已经将GENERIC_READ | GENERIC_WRITE传递给CreateFile |
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从系统的页文件中提交存储器。如果两个标志都不设定,其结果也一样。
4,5)dwMaximumSizeHigh和dwMaximumSizeLow这两个参数将告诉系统该文件的最大字节数
6)pszName: 它是个以0结尾的字符串,用于给该文件映射对象赋予一个名字。该名字用于与其他进程共享文件映射对象。
2.3 将文件数据映射到进程的地址空间
将文件的数据作为映射到该区域的物理存储器进行提交。
PVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap
);
1)hFileMappingObject:用于标识文件映射对象的句柄,该句柄是前面调用CreateFileMapping或OpenFileMapping函数返回的。
2)dwDesiredAccess:用于标识如何访问该数据。可以设定下表所列的4个值中的一个。
值 | 含义 |
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中,CreateileMapping函数可以用PAGE_READONLY、PAGE_READWRITE或PAGE_WRITECOPY等保护属性中的任何一个来调用。在Windows 98中,CreateFileMapping必须用PAGE_WRITECOPY来调用 |
(一个文件映射到你的进程的地址空间中时,你不必一次性地映射整个文件。相反,可以只将文件的一小部分映射到地址空间。被映射到进程的地址空间的这部分文件称为一个视图。)
3,4)dwFileOfsetHigh和dwFileOfsetLow参数。指定哪个字节应该作为视图中的第一个字节来映射。
5)dwNumberOfBytesToMap有多少字节要映射到地址空间。如果设定的值是0,那么系统将设法把从文件中的指定位移开始到整个文件的结尾的视图映射到地址空间。
2.4 从进程的地址空间中撤消文件数据的映像
当不再需要保留映射到进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:
BOOL UnmapViewOfFile(PVOID pvBaseAddress);
参数:pvBaseAddress由MapViewOfFile函数返回。
注意:如果没有调用这个函数,那么在进程终止运行前,保留的区域就不会被释放。每当调用MapViewOfFile时,系统总是在你的进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。
为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用FlushViewOfFile函数:
BOOL FlushViewOfFile(
PVOID pvAddress,
SIZE_T dwNumberOfBytesToFlush
);
1)pvAddress:包含在内存映射文件中的视图的一个字节的地址。
2)dwNumberOfBytesToFlush:指明你想要刷新的字节数。系统将把这个数字向上取整,使得字节总数是页面的整数。
如果你调用FlushViewOfFile函数并且不修改任何数据,那么该函数只是返回,而不将任何信息写入磁盘。
2.5 关闭文件映射对象和文件对象
用CloseHandle函数关闭相应的文件对象和文件映射对象句柄。
HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
CloseHandle(hFile);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);
CloseHandle(hFileMapping);
// Use the memory-mapped file.
UnmapViewOfFile(pvFile);
3、相关问题
3.1 内存映射文件与数据视图的相关性
系统允许映射一个文件的相同数据的多个视图。只要映射相同的文件映射对象,系统就会确保映射的视图数据的相关性。
例如,如果你的应用程序改变了一个视图中的文件内容,那么所有其他视图均被更新以反映这个变化。这是因为尽管页面多次被映射到进程的虚拟地址空间,但是系统只将数据放在单个RAM页面上。如果多个进程映射单个数据文件的视图,那么数据仍然是相关的,因为在数据文件中,每个RAM页面只有一个实例——正是这个RAM页面被映射到多个进程的地址空间。
注意:Windows允许创建若干个由单个数据文件支持的文件映射对象。Windows不能保证这些不同的文件映射对象的视图具有相关性。它只能保证单个文件映射对象的多个视图具有相关性。
3.2 页文件支持的内存映射文件
它不必调用CreateFile函数,只需要调用CreateFileMapping函数,并且传递INVALID_HANDLE_VALUE作为hFile参数。这将告诉系统,你不是创建其物理存储器驻留在磁盘上的文件中的文件映射对象,相反,你想让系统从它的页文件中提交物理存储器。分配的存储器的数量由CreateFileMapping函数的dwMaximumSizeHigh和dwMaximumSizeLow两个参数来决定。当创建了文件映射对象并且将它的一个视图映射到进程的地址空间之后,就可以像使用任何内存区域那样使用它。
如果你想要与其他进程共享该数据,可调用CreateFileMapping函数,并传递一个以0结尾的字符串作为pszName参数。然后,想要访问该存储器的其他进程就可以调用CreateFileMapping或OpenFileMapping函数,并传递相同的名字。
当进程不再想要访问文件映射对象时,该进程应该调用CloseHandle函数。当所有句柄均被关闭后,系统将从系统的页文件中收回已经提交的存储器。
3.3 用内存映射文件在进程之间共享数据
数据共享方法是通过让两个或多个进程映射同一个文件映射对象的视图来实现的,这意味着它们将共享物理存储器的同一个页面。因此,当一个进程将数据写入一个共享文件映射对象的视图时,其他进程可以立即看到它们视图中的数据变更情况。
注意:如果多个进程共享单个文件映射对象,那么所有进程必须使用相同的名字来表示该文件映射对象。