32_Win32 共享内存

Win32 共享内存

内存映射文件

很多文本编辑软件都提供了一个剪裁行尾空格的功能,例如EditPlus软件的编辑菜单项→格式→剪裁行尾空格。在调用ReadFile()函数读取文件时,每次读取一定大小的字节数例如64字节,这涉及一个缓冲区边界的问题。如果缓冲区的未尾有一个或多个空格,那么如何判断这些空格是单词之间的空格还是行尾的空格呢.因为缓冲区的末尾不一定正好是换行"\r\n",所以类似这样的问题并不能很好解决。我们可以一次读入整个文件,但是在Win32程序中可以使用的内存空间只有2GB大小(实际上无法申请这么大的内存),如果文件大小超过2GB怎么办呢?

内存映射文件提供了一组独立的函数,通过内存映射文件函数可以将磁盘上一个文件的全部或部分映射到进程虚拟地址空间的某个位置,完成映射后,程序就能够通过内存指针像访问内存一样对磁盘上的文件进行读写操作。

具体机制是,对磁盘文件所映射的内存的读写操作会通过系统底层自动实现对磁盘文件的读写。实际上还是需要把相关数据读入物理内存,类似于虚拟内存管理函数,内存映射文件会预订一块地址空间区域并在需要的时候提交页面。不同之处在于,内存映射文件的物理存储器来自磁盘上已有的文件,而不是系统的页面交换文件,实质上并没有省略什么环节,但是程序的结构将会从中受益,缓冲区边界等问题将不复存在。另外,内存映射文件会将硬盘上的文件不做修改地装载到内存中,内存中的文件和硬盘上的文件一样按字节顺序排列。但是,硬盘上的文件不一定按文件内容排列在一起,因为文件存储以簇为单位,整个文件内容可能会存储在不相邻的各个簇中;而通过内存映射文件读取到内存中的文件按线性排列,访问相对简单,访问速度得到了提升。

除此之外,以下两方面也用到了内存映射文件技术。

  • Windows操作系统加载、执行.exe和.dIl等可执行文件时也用到了内存映射文件技术,运行一个可执行文件时系统并不会把整个文件全部载入虚拟内存(页面交换文件和物理内存)中,这就节省了页面交换文件的空间以及应用程序后动所需的时间。
  • 使用内存映射文件还可以在同一台计算机上运行的多个进程之间共享数据,当一个进程改变了共享数据页的内容时,通过分页映射机制,其他进程的共享数据区的内容就会同时改变,因为它们实际上存储在同一个地方。许多进程间通信、同步机制在底层都是通过内存映射文件技术实现的。

使用内存映射文件的步骤通常如下。

  • 调用CreateFile函数创建或打开一个文件内核对象,返回一个文件对象句柄hFile,该对象标识了我们想要用作内存映射文件的磁盘文件。
  • 调用CreateFileMapping函数为hFile文件对象创建或打开一个文件映射内核对象,返回一个文件映射对象句柄hFileMap
  • 调用MapViewOfFile函数把文件映射对象hFileMap的部分或全部映射到进程的虚拟地址空间中,返回一个内存指针lpMemory,即可通过该指针来读写文件,这一步操作是映射文件映射对象的一个视图到虚拟地址空间中。

当不再需要内存映射文件时,应该执行以下清理工作。

  • 调用UnmapViewOfFile酗数取消对文件映射内核对象的映射,传入参数为lpMemory
  • 调用CloseHandle函数关闭文件映射内核对象,传入参数为hFileMap
  • 调用CloseHandle函数关闭文件内核对象,传入参数为hFile

内存映射文件相关函数

使用内存映射文件的第一步是调用CreateFile函数创建或打开一个文件内核对象,返回一个文件对象句柄hFile,该对象标识了我们想要用作内存映射文件的磁盘文件。然后,调用CreateFileMapping函数为hFile文件对象创建或打开一个文件映射内核对象,返回一个文件映射对象句柄hFileMap CreateFileMapping函数原型如下∶

HANDLE WINAPI CreateFileMapping
 (
	_ln_ HANDLE hFile,	//文件对象句柄
	_ln_opt_ LPSECURITY_ATTRIBUTES lpAttributes,//含义同其他内核对象的安全属性结构
	_ln_ DWORD flProtect,//文件映射对象的页面保护属性
	_ln_ DWORD dwMaximumSizeHigh, //文件映射对象大小的高32位,以字节为单位
	_ln_ DWORD dwMaximumSizeLow,//文件映射对象大小的低32位,以字节为单位
	_ln_opt_ LPCTSTR lpName //文件映射对象的名称,可为NULL
);

flProtect参数指定文件映射对象的页面保护属性,可以是下表所示的值之一,以下页面保护属性都包含一个写时复制属性,后面会介绍写时复制。

页面保护属性值页面保护属性含义
PAGE_READONLY Ox02完成对文件映射对象的映射时,文件具有可读和写时复制属性,必须使用GENERIC_READ访问权限创建hFile参数指定的文件
PAGE_READWRITE Ox04完成对文件映射对象的映射时,文件具有可读可写和写时复制属性,必须使用GENERIC_READ|GENERIC_WRITE访问权限创建hFile参数指定的文件
PAGE_EXECUTE_READ Ox20完成对文件映射对象的映射时,文件具有可读、可执行和写时复制属性,必须使用GENERIC_READ|GENERIC_EXECUTE访问权限创建hFile参数指定的文件
PAGE_EXECUTE_READWRITE Ox40完成对文件映射对象的映射时,文件具有可读可写、可执行和写时复制属性,必须使用GENERIC_READ|GENERIC_WRITE|GENERIC_EXECUTE访问权限创建hFile参数指定的文件
PAGE_WRITECOPY Ox08等效于PAGE_READONLYPAGE_EXECUTE_WRITECOPYO×80等效于 PAGE_EXECUTE_READ

接下来实现一个示例程序MemoryMappingFile,单击“打开文件"按钮,程序创建一个内存映射文件并将文件内容显示到多行编辑控件中;单击“追加数据"按钮,程序把单行编辑控件中的内容追加到文件中,并把新文件内容显示到多行编辑控件中,如图3.15所示。特别注意memcpy() 追加的时候,利用了内存地址的偏移来拷贝到文件的末尾处,这里需要注意一下,其他理解难度不大。

  • MemoryMappingFile.rc

在这里插入图片描述

  • resource.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 MemoryMappingFile.rc 使用
//
#define IDD_MAIN                        101
#define IDC_EDIT_PATH                   1001
#define IDC_EDIT_APPEND                 1002
#define IDC_BTN_OPEN                    1003
#define IDC_BTN_APPEND                  1004
#define IDC_EDIT_TEXT                   1005

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        103
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1006
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif
  • MemoryMappingFile.cpp
#include <windows.h>
#include <tchar.h>
#include "resource.h"

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
	return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	TCHAR szPath[MAX_PATH] = { 0 }; // 文件路径
	TCHAR szBuf[512] = { 0 };       // 追加数据
	LARGE_INTEGER liFileSize;
	HANDLE hFile;
	HANDLE hFileMap;
	LPVOID lpMemory;

	if (uMsg == WM_INITDIALOG)
	{
		//
		::SetDlgItemText(hwndDlg, IDC_EDIT_PATH, TEXT("D:\\Test.txt"));
		return TRUE;
	}
	else if (uMsg == WM_COMMAND)
	{

		if (LOWORD(wParam) == IDC_BTN_OPEN)
		{  

			//查输入框注入的文件路径
			::GetDlgItemText(hwndDlg, IDC_EDIT_PATH, szPath, _countof(szPath));

			//基于此路径,打开文件,得到文件句柄
			hFile = ::CreateFile(
				szPath,
				GENERIC_READ | GENERIC_WRITE,
				FILE_SHARE_READ, NULL,
				OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

			if (hFile == INVALID_HANDLE_VALUE)
			{
				::MessageBox(hwndDlg, TEXT("CreateFile函数调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}
			else
			{
				//根据文件句柄查文件大小,如果说文件没有内容则终止流程
				::GetFileSizeEx(hFile, &liFileSize);
				if (liFileSize.QuadPart == 0)
				{
					::MessageBox(hwndDlg, TEXT("文件大小为0"), TEXT("提示"), MB_OK);
					::CloseHandle(hFile);
					return TRUE;
				}
			}

			// 为hFile文件对象创建一个文件映射内核对象
			hFileMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
			if (!hFileMap)
			{
				MessageBox(hwndDlg, TEXT("CreateFileMapping调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}

			// 把文件映射对象hFileMap的全部映射到进程的虚拟地址空间中
			lpMemory = ::MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
			if (!lpMemory)
			{
				::MessageBox(hwndDlg, TEXT("MapViewOfFile调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}

			// 返回指向文件内容内存的指针,随机将把文件内容显示到编辑控件中
			SetDlgItemText(hwndDlg, IDC_EDIT_TEXT, (LPTSTR)lpMemory);

			// 清理工作
			::UnmapViewOfFile(lpMemory);
			::CloseHandle(hFileMap);
			::CloseHandle(hFile);

		}
		else if (LOWORD(wParam) == IDC_BTN_APPEND)
		{
			//查用户追加内容
			if (!GetDlgItemText(hwndDlg, IDC_EDIT_APPEND, szBuf, _countof(szBuf)))
			{
				::MessageBox(hwndDlg, TEXT("请输入追加内容"), TEXT("提示"), MB_OK);
				return FALSE;
			}

			//查输入框注入的文件路径
			GetDlgItemText(hwndDlg, IDC_EDIT_PATH, szPath, _countof(szPath));

			//基于此路径,打开文件,得到文件句柄
			hFile = ::CreateFile(szPath, GENERIC_READ | GENERIC_WRITE,
				FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
			if (hFile == INVALID_HANDLE_VALUE)
			{
				::MessageBox(hwndDlg, TEXT("CreateFile函数调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}

			// 为hFile文件对象创建一个文件映射内核对象
			// 查文件大小, 扩展文件到指定的大小
			::GetFileSizeEx(hFile, &liFileSize);
			hFileMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, liFileSize.HighPart,
				liFileSize.LowPart + _tcslen(szBuf) * sizeof(TCHAR), NULL);
			if (!hFileMap)
			{
				::MessageBox(hwndDlg, TEXT("CreateFileMapping调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}

			// 把文件映射对象hFileMap的全部映射到进程的虚拟地址空间中
			lpMemory = ::MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
			if (!lpMemory)
			{
				::MessageBox(hwndDlg, TEXT("MapViewOfFile调用失败"), TEXT("提示"), MB_OK);
				return TRUE;
			}

			// 写入追加数据
			memcpy_s((LPBYTE)lpMemory + liFileSize.QuadPart, _tcslen(szBuf) * sizeof(TCHAR),
				szBuf, _tcslen(szBuf) * sizeof(TCHAR));

			//刷新的虚拟地址空间中存的文件内容缓冲刷新到本地文件中
			::FlushViewOfFile(lpMemory, 0);
			
			
			// 把新文件内容显示到编辑控件中
			::SetDlgItemText(hwndDlg, IDC_EDIT_TEXT, (LPTSTR)lpMemory);

			// 清理工作
			::UnmapViewOfFile(lpMemory);
			::CloseHandle(hFileMap);
			::CloseHandle(hFile);
		}
		else if (LOWORD(wParam) == IDCANCEL)
		{
			::EndDialog(hwndDlg, 0);
		}
		return TRUE;
	}
	return FALSE;

}

在这里插入图片描述

写时复制

后动一个应用程序时,系统会调用CreateFile函数来打开磁盘上的.exe文件,然后调用CreateFileMapping函数来创建文件映射对象,并以新创建的进程的名义调用
MapViewOfFileEx (传入SEC_IMAGE标志)函数,这样就把.exe文件映射到了进程的地址空间中。之所以调用
MapViewOfFileEx而不是MapViewOfFile,是为了把文件映射到指定的基地址处,这个基地址保存在.exe文件的PE文件头中,然后系统创建进程的主线程,在映射得到的视图中取得可执行代码的起始地址,把该地址放到线程的指合指针中,最后由CPU开始执行其中的代码。如果用户后动同一个应用程序的第二个实例,则系统会发现该.exe文件已经有一个文件映射对象,因此不会再创建一个新的文件对象或文件映射对象,取而代之的是,系统会映射.exe文件的另一个视图,但这次是在新创建的进程的地址空间中,至此,系统已经把同一个.exe文件同时映射到了两个地址空间中。显然,由于物理内存中包含.exe文件可执行代码的那些页面为两个进程所共享,因此内存的利用率更高。

大概就是说,启动应用程序,CPU跑这些程序二进制代码的时候,会构建本地文件和虚拟地址空间做一次映射,这种启动多个应用程序的时候,可以通过映射,来复用这些二进制代码。从而节省内存的浪费。大概是以前没有这种机制的时候,启用一次exe文件,就把数据和代码载入内存,又启动又做一次一样的事情,很慢。所以,思考出来的一种解决方案。

从Windows Vista开始PE文件(可执行文件)支持动态基地址,在VS中通过鼠标右键单击项目名称,选择属性→配置属性→链接器→高级→随机基址和固定基址,可以看到默认情况下已经设置了随机基址,如果把随机基址设置为否,当运行可执行文件时,系统会把可执行文件默认映射到进程虚拟地址空间中Ox00400000的位置(即4MB,这是Windows 98系统中可执行文件能加载到的最低地址),如果不想使用这个地址,还可以指定其他固定基址。WinMain函数的hInstance参数的值表示的是这个基地址,系统会将可执行文件加载到虚拟地址空间的相应位置。

运行一个程序的多个实例,系统不会真正加载多份程序实例到内存中,每个程序实例只是可执行文件的一个内存映射视图,系统会共享一份程序的只读页面(程序可执行代码、只读数据),以及可写页面(例如全局变量、静态变量),但是采用了写时复制技术。Windows允许多个进程共享同一块内存,例如如果有10个记事本程序正在同时运行,所有的进程会共享程序的代码和数据,同一个程序的多个实例共享相同的内存页极大地提升了系统性能,但另一方面,这也要求所有的程序实例只能读取其中的数据或执行其中的代码,如果有一个程序实例修改并写入一个内存页,那么其他程序实例正在使用的内存页也会改变,这将导致混乱。因此,系统会给共享的可写内存页指定写时复制属性,当系统把一个.exe或.dII映射到进程地址空间时,系统会统计有多少页面是可写的,然后从页面交换文件中分配内存空间来容纳这些可写页面,除非有一个程序实例真的更改了可写页面,否则不会用到页面交换文件中的内存页。

当线程试图与人一个共享页面时,系统会从最初将模块映射到进程的地址空间时分配的页面交换文件页面中找到一个闲置页面,并为该闲置页面指定PAGE_READWRITE或
PAGE_EXECUTE_READWRITE保护属性,然后把线程想要修改的页面内容复制到闲置页面中,系统不会对原始页面的保护属性和数据做任何修改。然后,系统更新该进程的页面表,原来的虚拟内存地址即对应到内存中一个新的页面,系统在执行这些步骤后,这个进程即可访问它自己的副本。
内存映射文件同样用到了写时复制技术,因为文件映射对象是内核对象,可能有多个进程或线程同时使用同一个文件映射对象。写时复制是一种系统特性,各种场合都可能会用到该技术。比如,有一个大型数组,有一个自定义函数需要这个数组指针作为参数,除非函数要修改该数组的内容,否则没必要为酗数复制一份新数组。

通过内存映射文件在多个进程间共享数据

Windows提供了多种机制,使应用程序之间能够快速、方便地共享数据和信息,这些机制包括Windows消息、RPC、剪贴板、邮件槽(Mailslot)、管道(Pipe)、套接字(socket)等。在同一台机器上共享数据的最底层机制是内存映射文件,如果在同一台机器上的多个进程间进行通信,所有机制归根结底都会用到内存映射文件,如果要求低开销和高性能,内存映射文件无疑是最好的选择。这种数据共享机制通过两个或多个进程映射同一个文件映射对象的视图来实现,这意味着在进程间共享相同的内存页面,当一个进程在文件映射对象的视图中写入数据时,其他进程会在它们的视图中立刻看到变化。

在调用CreateFileMapping函数时,如果把hFile参数设置为INVALID_HANDLE_VALUE,系统会使用页面交换文件创建一个文件映射对象,基于页面交换文件的文件映射对象通常用于进程间共享数据。下面实现一个通过内存映射文件在进程间共享数据的示例MemoryMappingFile_Process,我们可以后动本程序的多个实例,在任何一个程序实例的编辑框中输入内容时,当前程序实例编辑控件中的内容会立即显示到所有程序实例的静态控件中,程序运行效果如图下所示

  • MemoryMappingFile_Process.rc

在这里插入图片描述

  • resource.h
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 MemoryMappingFile_Process.rc 使用
//
#define IDD_MAIN                        101
#define IDC_EDIT_SHARE                  1001
#define IDC_STATIC_SHARE                1002

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        103
#define _APS_NEXT_COMMAND_VALUE         40001
#define _APS_NEXT_CONTROL_VALUE         1003
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif
  • MemoryMappingFile_Process.cpp
#include <windows.h>
#include "resource.h"

#define BUF_SIZE 4096

// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ::DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
    return 0;
}

INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{

    static HANDLE hFileMap; //静态全局
    static LPVOID lpMemory;

    if (uMsg == WM_INITDIALOG)
    {
        // 创建或打开一个 命名文件映射内核对象,BUF_SIZE字节
        hFileMap = ::CreateFileMapping(
            INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
            0, BUF_SIZE, TEXT("2F4368E6-09A1-4D5E-ACC9-C1BDBB041BF7"));
      
        if (!hFileMap)
        {
            ::MessageBox(hwndDlg, TEXT("CreateFileMapping调用失败"), TEXT("提示"), MB_OK);
            return TRUE;
        }

        // 创建一个计时器,每一秒钟在静态控件中刷新显示一次共享数据
        ::SetTimer(hwndDlg, 1, 1000, NULL);
        return TRUE;

    }
    else if (uMsg == WM_COMMAND)
    {
        if (LOWORD(wParam) == IDC_EDIT_SHARE)
        {
            // 如果,编辑控件中的内容改变
            if (HIWORD(wParam) == EN_UPDATE)
            {
                //查编剧框内容 存到缓冲空间 
               ::GetDlgItemText(hwndDlg, IDC_EDIT_SHARE, (LPTSTR)lpMemory, BUF_SIZE);
            }
        }
        else if (LOWORD(wParam) == IDCANCEL)
        {  
            
            // 清理工作
            KillTimer(hwndDlg, 1);
            UnmapViewOfFile(lpMemory);
            CloseHandle(hFileMap);
            EndDialog(hwndDlg, 0);

        }
        else if (LOWORD(wParam) == WM_TIMER)
        {
            //定时器,把内容显示给静态控件
            ::SetDlgItemText(hwndDlg, IDC_STATIC_SHARE, (LPTSTR)lpMemory);
        }

    }

    return FALSE;
}

/*
GUID guid;
TCHAR szGUID[64] = { 0 };

// 生成一个GUID
CoCreateGuid(&guid);
// 转换为字符串
wsprintf(szGUID, TEXT("%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X"),
    guid.Data1, guid.Data2, guid.Data3,
    guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
    guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
*/

在WM_INITDIALOG消息中,程序调用CreateFileMapping酗数创建或打开一个4096字节大小的命名文件映射对象,并调用MapViewOfFile函数把文件映射对象hFileMap的全部映射到进程的虚拟地址空间中,得到一个共享内存指针
lpMemory,然后创建一个计时器,每一秒钟在静态控件中刷新显示一次共享内存的数据。读者可以运行本程序的多个实例进行测试。每当编辑控件中的内容改变时,程序读取编辑控件中的内容到共享内存lpMemory中,在WM_TIMER消息中,程序把共享内存的内容显示到静态控件中。

关闭对话框时,程序调用UnmapViewOfFile函数撤销内存映射,调用CloseHandle函数关闭文件映射对象句柄,一个程序实例关闭文件映射对象句柄并不会影响其他实例继续使用文件映射对象。如前所述,所有内核对象的数据结构中通常都包含安全描述符和引用计数字段,创建或打开一个文件映射对象都会导致引用计数加1,而调用CloseHandle函数则会导致引用计数减1,只要引用计数不为0,系统就不会销毁它。

如果要创建一个命名文件映射对象,而系统中已经存在一个相同名称的其他内核对象,例如互斥量(Mutex)对象、信号量(Semaphore)对象等,那么调用CreateFileMapping函数创建该名称的文件映射对象会失败,返回值为NULL,调用GetLastError函数返回ERROR_INVALID_HANDLE,因此创建命名文件映射对象时应该保证名称在系统中唯一。本程序的文件映射对象名称使用了一个GUID字符串。如果需要生成一个GUID,可以在VS中单击工具菜单→创建GUID(G)命合。如果需要在程序中动态生成一个GUID,可以调用
CoCreateGuid函数,例如下面的代码∶

GuID guid;
TCHAR SzGUID[64]= { 0 };生成一个GUID
CoCreateGuid(&guid);//转换为字符串
wsprintf(szGUID,TEXT("%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X"),
guid.Datal, guid.Data2, guid.Data3,
guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);

使用内存映射文件来处理大型文件

使用内存映射文件的第3步为∶调用MapViewOfFile函数把文件映射对象hFileMap的部分或全部映射到进程的虚拟地址空间中,返回一个内存指针lpMemory,即可通过该指针来读写文件,这一步操作是映射文件映射对象的一个视图到虚拟地址空间中。但是对32位进程来说,可用的用户模式地址空间只有2G,超过2G的内存映射文件无法一次性全部映射到进程的虚拟地址空间中。对于大型文件,可以采取多次映射的方法,在调用CreateFileMapping函数为hFile文件对象创建或打开一个文件映射内核对象后,每次调用MapViewOfFile函数只映射一部分文件映射对象,完成对已映射部分的访问后,我们可以撤销对这一部分的映射,然后把文件映射对象的另一部分映射到视图中,一直重复这个过程,直到完成对整个文件映射对象的访问。需要注意的是,MapViewOfFile函数的文件映射对象偏移量参数必须指定为内存分配粒度的整数倍。下面的自定义函数CopyLargeFile实现了对大型文件的复制操作︰

//拷贝大型文件 
BOOL CopyLargeFile(LPCTSTR srcFilePath /*源文件*/, LPCTSTR destFilePath /*目标文件*/)
{
    HANDLE hFile1, hFile2, hFileMap;
    LPVOID lpMemory;

    //打开文件1
    hFile1 = ::CreateFile(
        srcFilePath,
        GENERIC_READ, FILE_SHARE_READ,
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

    if (hFile1 == INVALID_HANDLE_VALUE)
    {

        return FALSE;
    }

    //创建文件2
    hFile2 = ::CreateFile(
        destFilePath,
        GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
        NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile2 == INVALID_HANDLE_VALUE)
    {
        return FALSE;
    }

    //为hFile1文件对象创建一个文件映射内核对象
    hFileMap = ::CreateFileMapping(hFile1, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hFileMap)
    {
        return FALSE;
    }

    //获取文件大小,内存分配粒度
    _int64 qwFileSize;
    DWORD dwFileSizeHigh;
    SYSTEM_INFO si = {0};
    qwFileSize = ::GetFileSize(hFile1, &dwFileSizeHigh);
    qwFileSize += (((_int64)dwFileSizeHigh) << 32);

    ::GetSystemInfo(&si);

    //把文件映射对象hFileMap不断映射到进程的虚拟地址空间中
    _int64 qwFileOffset = 0; //文件映射对象偏移量
    DWORD dwBytesInBlock; //本次映射大小
    while (qwFileSize > 0)
    {

        dwBytesInBlock = si.dwAllocationGranularity; //可以分配虚拟内存的起始地址的粒度
        if (qwFileSize < dwBytesInBlock)
        {
            dwBytesInBlock = (DWORD)qwFileSize;
        }

        lpMemory = ::MapViewOfFile(hFileMap, FILE_MAP_READ,
            (DWORD)(qwFileOffset >> 32), (DWORD)
            (qwFileOffset & 0xFFFFFFFF), dwBytesInBlock);


        if (!lpMemory)
        {
            return FALSE;
        }
        //对已映射部分进行操作
        ::WriteFile(hFile2, lpMemory, dwBytesInBlock, NULL, NULL);

        //取消本次映射,进行下一轮映射
        UnmapViewOfFile(lpMemory);
        qwFileOffset += dwBytesInBlock;
        qwFileSize -= dwBytesInBlock;

    }


    //清理工作
    ::CloseHandle(hFileMap);
    ::CloseHandle(hFile1);
    ::CloseHandle(hFile2);
    return TRUE;


}

在上面的示例中,每次只映射内存分配粒度大小,除文件复制操作外,读者可以根据需要对大型文件进行各种操作。

CFileMappingHelper封装

  • CFileMappingHelper.hpp
#pragma once
#include <wtypesbase.h>
#include <vector>
#include <string>
#include <map>
#include <tchar.h>

#pragma warning(disable:4200)

#ifdef _UNICODE
using _tstring = std::wstring;
#else
using _tstring = std::string;
#endif

class CFileMappingHelper
{
public:
    CFileMappingHelper();
    ~CFileMappingHelper();
    CFileMappingHelper(const CFileMappingHelper&) = delete;
    CFileMappingHelper& operator = (const CFileMappingHelper&) = delete;    //删除拷贝构造与赋值重载

    //
    // @brief: 创建文件映射
    // @param: strName    文件映射名
    // @param: strMutex   互斥锁名
    // @param: dwSize     缓冲大小
    // @ret: bool
    bool Create(
        LPCTSTR strName = nullptr,
        LPCTSTR strMutex = nullptr,
        DWORD dwSize = 4096
    );

    //
    // @brief: 销毁释放资源
    // @param: 无
    // @ret: void
    void Clear();

    //
    // @brief: 是否已创建
    // @param: 无
    // @ret: bool           初始化与否
    bool IsCreate() const;

    //
    // @brief: 获取文件映射容量
    // @ret: DWORD          文件映射容量
    DWORD GetCapacity() const;

    //
    // @brief: 写入文件映射
    // @param: lpData       要写入数据
    // @param: dwSize       写入长度
    // @ret: bool           操作成功与否
    bool Write(LPCVOID lpData, DWORD dwSize);

    //
    // @brief: 读取文件映射
    // @param: lpData       读入的缓冲
    // @param: dwSize       缓冲长度
    // @param: lpBytesRead  成功读取的长度
    // @ret: bool           操作成功与否
    bool Read(LPVOID lpData, DWORD dwSize, LPDWORD lpBytesRead = nullptr);

private:

    HANDLE m_hFileMapping; //文件映射对象句柄
    LPTSTR m_hData;//指向共享内存的指针
    HANDLE m_hMutex; //互斥锁对象句柄
    bool m_bInit; //记录是不是首次初始化
};

  • CFileMappingHelper.cpp
#include "CFileMappingHelper.hpp"

typedef struct _FILE_MAPPING_DATA
{

	DWORD dwCapacity = 0;       //最大容量
	DWORD dwPid = 0;            //进程ID
	DWORD dwSize = 0;           //数据大小
	BYTE Data[0];               //数据内容


}FILE_MAPPING_DATA, * PFILE_MAPPING_DATA;

CFileMappingHelper::CFileMappingHelper()
	:m_hFileMapping(nullptr),
	m_hData(nullptr),
	m_hMutex(nullptr),
	m_bInit(false)
{

}

CFileMappingHelper::~CFileMappingHelper()
{
	Clear();
}

bool CFileMappingHelper::Create(LPCTSTR strName, LPCTSTR strMutex, DWORD dwSize)
{
	SECURITY_ATTRIBUTES sa = { 0 };
	SECURITY_DESCRIPTOR sd = { 0 };
	bool bIsSuccess = false;
	bool bIsFirstCreate = false;
	if (m_bInit)
	{
		return true;
	}
	sa.nLength = sizeof(sa);
	sa.bInheritHandle = FALSE;
	sa.lpSecurityDescriptor = &sd;

	(void)::InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
	(void)::SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);

	do
	{
		m_hMutex = ::CreateMutex(&sa,FALSE,strMutex);
		if (nullptr == m_hMutex)
		{
			break;
		}
		//打开一个文件映射内核对象,得到文件映射对象句柄
		m_hFileMapping = ::CreateFileMapping(INVALID_HANDLE_VALUE,&sa,
			PAGE_READWRITE,0,dwSize + sizeof(FILE_MAPPING_DATA),strName);
		if (nullptr == m_hFileMapping)
		{
			break;
		}
		//查是不是第一次创建的文件映射内核对象
		if ((nullptr != m_hFileMapping) && (ERROR_ALREADY_EXISTS != ::GetLastError()))
		{
			bIsFirstCreate = true;
		}

		//将文件映射对象映射到进程的虚拟地址空间中
		m_hData = (LPTSTR)::MapViewOfFile(
			m_hFileMapping, 
			FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
		
		if (nullptr == m_hData)
		{
			break;
		}

		//首次创建 设置信息
		if (bIsFirstCreate)
		{
			PFILE_MAPPING_DATA pData = reinterpret_cast<PFILE_MAPPING_DATA>(m_hData);
			pData->dwCapacity = dwSize;
			pData->dwPid = ::GetCurrentProcessId();
		}

		bIsSuccess = true;
	} while (false);

	if (!bIsSuccess)
	{
		Clear();
	}
	m_bInit = bIsSuccess;
	return bIsSuccess;
}

void CFileMappingHelper::Clear()
{
	if (false == m_bInit)
	{
		return;
	}

	if (m_hData)
	{
		::UnmapViewOfFile(m_hData);
		m_hData = nullptr;
	}

	if (m_hFileMapping)
	{
		::CloseHandle(m_hFileMapping);
		m_hFileMapping = nullptr;
	}
	if (m_hMutex)
	{
		::CloseHandle(m_hMutex);
		m_hMutex = nullptr;
	}

	m_bInit = false;
}

bool CFileMappingHelper::IsCreate() const
{
	return m_bInit;
}

DWORD CFileMappingHelper::GetCapacity() const
{
	if ((nullptr == m_hMutex) || (nullptr == m_hData))
	{
		return 0;
	}

	PFILE_MAPPING_DATA pData = reinterpret_cast<PFILE_MAPPING_DATA>(m_hData);
	return pData->dwCapacity;
}

bool CFileMappingHelper::Write(LPCVOID lpData, DWORD dwSize)
{
	bool bIsSuccess = false;
	DWORD dwWait = WAIT_OBJECT_0;

	if ((nullptr == m_hMutex) || (nullptr == m_hData) || (nullptr == lpData) || (0 == dwSize))
	{
		return false;
	}
	PFILE_MAPPING_DATA pData =  reinterpret_cast<PFILE_MAPPING_DATA>(m_hData);
	do
	{

		dwWait = ::WaitForSingleObject(m_hMutex, INFINITE);
		if (dwWait != WAIT_OBJECT_0)
		{
			break;
		}

		if (dwSize <= pData->dwCapacity)
		{
			
			::memcpy_s(pData->Data, pData->dwCapacity, lpData, dwSize);
			pData->dwSize = dwSize;
			bIsSuccess = true;
			
		}

	} while (false);

	::ReleaseMutex(m_hMutex);

	return bIsSuccess;

}

bool CFileMappingHelper::Read(LPVOID lpData, DWORD dwSize, LPDWORD lpBytesRead)
{
	bool bIsSuccess = false;
	DWORD dwWait = WAIT_OBJECT_0;

	if ((nullptr == m_hMutex) || (nullptr == m_hData) || (nullptr == lpData) || (0 == dwSize))
	{
		return false;
	}
	PFILE_MAPPING_DATA pData = reinterpret_cast<PFILE_MAPPING_DATA>(m_hData);
	do
	{
		dwWait = ::WaitForSingleObject(m_hMutex, INFINITE);
		if (dwWait != WAIT_OBJECT_0)
		{
			break;
		}
		if (dwSize <= (pData->dwCapacity))
		{
			::memcpy_s(lpData,dwSize,pData->Data,pData->dwSize);
			if (nullptr != lpBytesRead)
			{
				*lpBytesRead = pData->dwSize;
			}
			bIsSuccess = true;
		}
	} while (false);

	::ReleaseMutex(m_hMutex);
	return bIsSuccess;
}