与虚拟内存相似,内存映射文件保留了一个地址空间区域,在需要时将它提交到物理存储器。它们之间的不同点是内存映射文件提交到物理存储器的数据来自磁盘上相应的文件,而不是系统页文件。一旦文件被映射,就可以认为整个文件被加载到了内存中,可以像访问内存一样访问文件的内容。
使用内存映射文件的目的有3个:
(1)系统使用内存映射文件来加载和执行.EXE和DLL文件。这极大的节省了系统页文件空间,也缩短了启动应用程序所需的时间。
(2)使用内存映射文件访问磁盘上的数据。这既避免了对文件执行文件I/O(输入/输出)操作,也避免了为文件的内容申请缓冲区。
(3)使用内存映射文件在多个进程间共享数据。Windows也提供了其他进程间通信的方法——但是这些方法都是使用内存映射文件实现的,所以内存映射文件是最有效的方法。
8.4.1 内存映射文件相关函数
内存映射文件的函数包括CreateFileMapping、OpenFileMapping、MapViewOfFile、UnmapViewOfFile和FlushViewOfFile。
使用内存映射文件可以分为两步,第一步是使用CreateFileMapping创建一个内存映射文件内核对象,告诉操作系统内存映射文件需要的物理内存大小。这个步骤决定了内存映射文件的用途——究竟是为磁盘上的文件建立内存映射还是为多个进程共享数据建立共享内存。CreateFileMapping函数可以创建或者打开一个命名的或未命名的映射文件对象,用法如下。
HANDLE CreateFileMapping(
HANDLE hFile, // 一个文件的句柄
LPSECURITY_ATTRIBUTES lpAttributes, // 定义内存映射文件对象是否可以继承
DWORD flProtect, // 该内存映射文件的保护类型
DWORD dwMaximumSizeHigh, // 内存映射文件的长度
DWORD dwMaximumSizeLow,
LPCTSTR lpName // 内存映射文件的名字
);
函数的第一个参数hFile指定要映射的文件的句柄,如果这是一个已经打开的文件的句柄(CreateFile函数的返回值),那么将建立这个文件的内存映射文件;如果这个参数是-1,那么将建立共享内存。
第三个参数flProtect指定内存映射文件的保护类型,它的取值可以是PAGE_ READONLY(内存页面是只读的)或PAGE_READWRITE(内存页面可读写)。
dwMaximumSizeHigh和dwMaximumSizeLow参数组合指定了一个64位的内存映射文件的长度。一种简单的方法是将这两个参数全部设置位0,那么内存映射文件的大小将与磁盘上的文件相一致。
如果创建的是共享内存,其他进程不能再使用CreateFileMapping函数去创建同名的内存映射文件对象,而要使用OpenFileMapping函数去打开已创建好的对象,函数用法如下。
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, // 指定保护类型
BOOL bInheritHandle, // 返回的句柄是否可被继承
LPCTSTR lpName // 创建对象时使用的名字
);
dwDesiredAccess参数指定的保护类型有FILE_MAP_WRITE和FILE_MAP_READ,分别为可写属性和可读属性。
如果CreateFileMapping和OpenFileMapping函数执行成功,返回的是内存映射文件句柄;如果函数执行失败则返回NULL。
使用内存映射文件的第二步是映射文件映射对象的全部或者一部分到进程的地址空间。可以认为该操作是为文件中的内容分配线性地址空间,并将线性地址和文件内容对应起来。完成这项操作的函数是MapViewOfFile,它的用法如下。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 前两个函数返回的内存映射文件对象的句柄
DWORD dwDesiredAccess, // 指定保护类型,可以是FILE_MAP_WRITE、FILE_MAP_READ
DWORD dwFileOffsetHigh, // 从文件的那个地址开始映射
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap // 要映射的字节数,如果指定为0则映射整个文件
);
如果映射成功,函数返回映射视图的内存地址。失败则返回NULL。
当不使用内存映射文件时,可以通过UnmapViewOfFile函数撤销映射并使用CloseHandle函数关闭内存映射文件的句柄。
BOOL UnmapViewOfFile (LPCVOID lpBaseAddress);
如果修改了映射视图中的内存,系统会在试图撤销映射或文件映射对象被删除时自动将数据写到磁盘上,但程序也可以根据需要将视图中的数据立即写到磁盘上,完成该功能的函数是FlushViewOfFile。
BOOL FlushViewOfFile(
LPCVOID lpBaseAddress, // 开始的地址
SIZE_T dwNumberOfBytesToFlush // 数据块的大小
);
8.4.2 使用内存映射文件读BMP文件的例子
内存映射文件的功能之一是将磁盘上的整个文件读入内存,应用程序直接访问这块内存就相当于访问文件的内容了。这对于从比较大的文件中读取信息来说相当方便。比如,要将磁盘上的BMP文件(位图文件)读入内存,并在屏幕上将它显示出来,就必须分析BMP文件的文件结构,此时可以首先使用内存映射文件把整个BMP文件读入内存,然后再访问这个内存块。使用内存映射文件读文件的过程如下:
(1)调用CreateFile函数打开想要映射的文件,得到文件句柄hFile。
(2)调用CreateFileMapping函数,并传入文件句柄hFile,为该文件创建一个内存映射内核对象,得到内存映射文件句柄hMap。
(3)调用MapViewOfFile函数映射整个文件到内存。该函数返回文件映射到内存后的内存地址。使用指向这个地址的指针就可以读取文件中的内容了。
(4)调用UnmapViewOfFile函数来解除文件映射。
(5)调用CloseHandle函数关闭内存映射文件对象,必须传入内存映射文件句柄hMap。
(6)调用CloseHandle函数关闭文件对象,必须传入文件句柄hFile。
BMP文件又称位图文件,它是存储在电脑上的未经压缩的图片。因为它没有被压缩,所以BMP文件显示出来的图像是最清晰的,其他格式的图片文件基本都是在BMP文件基础上压缩得到的。在图像识别和图像处理等领域,BMP文件是最重要的。
从磁盘上读取BMP文件显示给用户的过程如下。
(1)建立与应用程序窗口相兼容的内存DC,建立一个与磁盘BMP文件大小相同的、与窗口客户区DC兼容的内存Bitmap(位图)。
(2)将这个内存Bitmap选入到内存DC中,这样应用程序就可以在内存DC上进行绘图操作了。
(3)通过分析BMP文件的格式(文件头结构和信息头结构),将这个BMP文件画到内存DC中。
(4)最后在窗口处理WM_PAINT消息时将内存DC复制到客户区DC中。
1.创建内存DC和内存Bitmap(位图)
CreateComaptibleDC函数用于创建一个与指定设备环境兼容的内存设备环境。
HDC CreateCompatibleDC(HDC hdc); // hdc参数为一个存在的设备环境句柄
CreateComaptibleDC 函数创建了一个与指定的DC兼容的内存DC。参数中的hdc只是个参考DC,该函数创建这个DC的内存映像。
内存DC仅仅存在在内存中。当内存DC刚被创建时,它仅有黑白两色。在应用程序使用内存DC进行绘图操作时,它必须首先选入一个位图到这个DC中。进行这个操作的函数是CreateCompatibleBitmap,它创建了一个与指定设备环境兼容的位图。这个位图能够被选入到任何的内存DC中。函数用法如下。
HBITMAP CreateCompatibleBitmap(
HDC hdc, // 参考DC的设备环境句柄,必须与CreateCompatibleDC函数使用的参考句柄相兼容
int nWidth, // 位图的宽度
int nHeight // 位图的高度
);
CreateCompatibleBitmap函数创建的位图的颜色格式与参考DC的格式相同。它的返回值是位图句柄,可以使用SelectObject函数选入此句柄到上面创建的内存DC中。
2.在DC间复制图像
在内存DC上绘制完图形之后,为了向用户显示,可以调用BitBlt函数把内存DC上的位图复制到真正的DC中。这就是在屏幕上快速显示图像的双缓冲技术(可以减少图像抖动)。
BitBlt是一个重要的块传送操作函数。块传送指把源位置中的数据块按照指定的方式传送到目的位置中。把内存中的位图复制到窗口客户区以及在不同的DC间复制图形数据都要用到块传送操作。下面举例说明BitBlt函数的用法。
在一个窗口程序中,用下面的代码响应WM_PAINT消息,将把窗口左上角的图标,复制到客户区。
void CMainWindow::OnPaint()
{
CPaintDC dcClient(this); // 客户区 DC (目标 DC)
CWindowDC dcWindow(this); // 整个窗口 DC (源 DC)
// 将窗口左上角30×30大小的图像拷贝到客户区
::BitBlt(
dcClient, // hdcDst 目标 DC
10, // xDst 指定目标 DC 中接受图像的起始位置(xDst, yDst)
10, // yDst
30, // cx 欲传输图象的宽度(cx)和高度(cy)
30, // cy
dcWindow, // hdcSrc 源 DC
0, // xSrc 指定源 DC 中要拷贝的图像的起始坐标(xSrc,ySrc)
0, // ySrc
SRCCOPY); // dwROP 传输过程要执行的光栅运算
}
BitBlt是“数据块传送”的意思,即“Bit Block Transfer”。这个函数可以将一个DC 中的图像拷贝到另一个DC 中,而不会改变图像的大小。
3.BMP文件结构
为了将BMP文件读到内存DC中,必须要了解文件的内部结构,下面进行简单介绍。
BMP文件的开始是BITMAPFILEHEADER结构(文件头),它的定义如下。
typedef struct tagBITMAPFILEHEADER {
WORD bfType; // 文件标识,必需为BM
DWORD bfSize; // 文件长度
WORD bfReserved1; // 0
WORD bfReserved2; // 0
DWORD bfOffBits; // 位图数据在文件中的起始位置
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;
BITMAPFILEHEADER结构的后面是BITMAPINFOHEADER结构(信息头)。
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; // 本结构的长度
LONG biWidth; // 位图的宽度
LONG biHeight; // 位图的高度
WORD biPlanes; // 位图的色平面数
WORD biBitCount; // 位图的颜色深度 (一个象素所占用的位数)
DWORD biCompression; // 位图的压缩方式
DWORD biSizeImage; // 位图的尺寸
LONG biXPelsPerMeter; // 图形x方向的分辨率,单位是像素/米
LONG biYPelsPerMeter; // 图形y方向的分辨率,单位是像素/米
DWORD biClrUsed; // 指定了颜色表的大小
DWORD biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
之后是颜色表(如果有的话)。位图信息BITMAPINFO是由BITMAPINFOHEADER结构和颜色表组成的。
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 位图信息头
RGBQUAD bmiColors[1]; // 颜色表
} BITMAPINFO;
知道了这些信息对读取BMP文件来说已经足够了。但是如果想保存显示设备上的一块区域到BMP文件中,还需要更加详细地了解BITMAPINFOHEADER结构中各域的作用。
4.例子代码
下面是一个浏览BMP文件的例子(08ReadBMP工程下)
// ReadBMP.h文件
#include <afxwin.h>
class CMyApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};
class CMainWindow : public CWnd
{
public:
CMainWindow();
protected:
HDC m_hMemDC; // 与客户区兼容的内存DC句柄
int m_nWidth; // BMP图像的宽度
int m_nHeight; // BMP图像的高度
protected:
virtual void PostNcDestroy();
afx_msg BOOL OnCreate(LPCREATESTRUCT);
afx_msg void OnPaint();
afx_msg void OnDestroy();
afx_msg void OnFileOpen();
DECLARE_MESSAGE_MAP()
};
// ReadBMP.cpp
#include <afxdlgs.h>
#include "resource.h"
#include "ReadBMP.h"
CMyApp theApp;
BOOL CMyApp::InitInstance()
{
m_pMainWnd = new CMainWindow;
m_pMainWnd->ShowWindow(m_nCmdShow);
return TRUE;
}
CMainWindow::CMainWindow()
{
LPCTSTR lpszClassName = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW,
::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_WINDOW+1), theApp.LoadIcon(IDI_MAIN));
CreateEx(NULL, lpszClassName, "BMP文件浏览器",
WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL);
}
BEGIN_MESSAGE_MAP(CMainWindow, CWnd)
ON_WM_CREATE()
ON_WM_PAINT()
ON_COMMAND(FILE_OPEN, OnFileOpen) // 文件菜单项下的子项“打开”的ID号为FILE_OPEN
END_MESSAGE_MAP()
void CMainWindow::PostNcDestroy()
{
delete this;
}
BOOL CMainWindow::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
::SetMenu(m_hWnd, ::LoadMenu(theApp.m_hInstance, (LPCTSTR)IDR_MAIN));
CClientDC dc(this);
// 初始化内存DC
m_hMemDC = ::CreateCompatibleDC(dc);
m_nHeight = 0;
m_nWidth = 0;
return TRUE;
}
void CMainWindow::OnPaint()
{
CPaintDC dc(this);
::BitBlt(dc, 0, 0,
m_nWidth, m_nHeight, m_hMemDC, 0, 0, SRCCOPY);
/* CPaintDC dcClient(this); // 客户区 DC (目标 DC)
CWindowDC dcWindow(this); // 整个窗口 DC (源 DC)
// 将窗口左上角30×30大小的图像拷贝到客户区
::BitBlt(
dcClient, // hdcDst 目标 DC
10, // xDst 指定目标 DC 中接受图像的起始位置(xDst, yDst)
10, // yDst
30, // cx 欲传输图象的宽度(cx)和高度(cy)
30, // cy
dcWindow, // hdcSrc 源 DC
0, // xSrc 指定源 DC 中要拷贝的图像的起始坐标(xSrc,ySrc)
0, // ySrc
SRCCOPY); // dwROP 传输过程要执行的光栅运算
*/
}
void CMainWindow::OnDestroy()
{
::DeleteDC(m_hMemDC);
}
void CMainWindow::OnFileOpen() // 用户点击打开菜单命令时
{
CFileDialog file(TRUE);
if(!file.DoModal())
return;
// 下面是映射BMP文件到内存的过程
// 打开要映射的文件
HANDLE hFile = ::CreateFile(file.GetFileName(), GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
MessageBox("读取文件出错!");
return;
}
// 创建内存映射对象
HANDLE hMap = ::CreateFileMapping(hFile, NULL, PAGE_READONLY, NULL, NULL, NULL);
// 映射整个BMP文件到内存,返回这块内存的首地址
LPVOID lpBase = ::MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
// 下面是获取BMP文件信息的过程
BITMAPFILEHEADER *pFileHeader; // bitmap file-header
BITMAPINFO *pInfoHeader; // bitmap info-header
// 取得file-header指针,以获得位图象素
pFileHeader = (BITMAPFILEHEADER*)lpBase;
if(pFileHeader->bfType != MAKEWORD('B', 'M'))
{
MessageBox("本程序仅读取BMP文件!");
::UnmapViewOfFile(lpBase);
::CloseHandle(hMap);
::CloseHandle(hFile);
return;
}
BYTE *pBits = (BYTE*)lpBase + pFileHeader->bfOffBits;
// 取得info-header指针,以获得文件的大小
pInfoHeader = (BITMAPINFO*)((BYTE*)lpBase + sizeof(BITMAPFILEHEADER));
m_nHeight = pInfoHeader->bmiHeader.biHeight;
m_nWidth = pInfoHeader->bmiHeader.biWidth;
// 下面是显示BMP文件到内存设备的过程
CClientDC dc(this);
// 创建一个与指定DC兼容的未初始化的位图,选入到内存兼容DC中
HBITMAP hBitmap = ::CreateCompatibleBitmap(dc, m_nWidth, m_nHeight);
::SelectObject(m_hMemDC, hBitmap);
// 把象图像据放到建立的设备中
int nRet = ::SetDIBitsToDevice(m_hMemDC,
0, // xDest
0, // yDest
m_nWidth,
m_nHeight,
0, // xSrc
0, // ySrc
0, // uStartScan 开始复制的扫描线和要复制的扫描线数
m_nHeight, // cScanLines
pBits, // lpvBits 指向DIB中的象素数据部分
pInfoHeader, // lpbmi 指向BITMAPINFO结构
DIB_RGB_COLORS); // fuColorUse 指定了DIB中数据的类型
::InvalidateRect(m_hWnd, NULL, TRUE);
::DeleteObject(hBitmap);
::UnmapViewOfFile(lpBase);
::CloseHandle(hMap);
::CloseHandle(hFile);
}
SetDIBitsToDevice函数将BMP文件中的图像数据设置到一个设备环境中的指定区域里。函数要求从BMP文件中得到的信息有图像的高度、宽度、位图像素数据和BITMAPINFO结构的指针。这些信息都可以从BMP文件的文件头和信息头两个数据结构中取得。
程序读取BMP文件信息时使用了内存映射文件,与前面读取PE文件信息的程序相比方便了许多。再也不用去自己申请内存了,整个文件都会被映射到内存中,可以像操作内存一样去操作文件。
8.4.3 进程间共享内存
内存映射文件的另一个功能是在进程间共享数据,它提供了不同进程共享内存的一个有效且简单的方法。后面的许多例子都要用到共享内存。
共享内存主要是通过映射机制实现的。
Windows下进程的地址空间在逻辑上是相互隔离的,但在物理上却是重叠的。所谓的重叠是指同一块内存区域可能被多个进程同时使用。当调用CreateFileMapping创建命名的内存映射文件对象时,Windows即在物理内存申请一块指定大小的内存区域,返回文件映射对象的句柄hMap。为了能够访问这块内存区域必须调用MapViewOfFile 函数,促使Windows 将此内存空间映射到进程的地址空间中。当在其他进程访问这块内存区域时,则必须使用 OpenFileMapping函数取得对象句柄hMap,并调用MapViewOfFile函数得到此内存空间的一个映射。这样一来,系统就把同一块内存区域映射到了不同进程的地址空间中,从而达到共享内存的目的。
下面举例说明如何将内存映射文件用于共享内存。
第一次运行这个例子时,它创建了共享内存,并写入数据“123456”。只要创建共享内存的进程没有关闭句柄hMap,以后运行的程序就会读出共享内存里面的数据,并打印出来。这就是使用共享内存在进程间通信的过程。08ShareMem程序代码如下。
// ShareMem.cpp文件
#include <stdio.h>
#include <windows.h>
void main()
{
char szName[] = "08ShareMem"; // 内存映射对象的名称
char szData[] = "123456"; // 共享内存中的数据
LPVOID pBuffer; // 共享内存指针
// 首先试图打开一个命名的内存映射文件对象
HANDLE hMap = ::OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szName);
if(hMap != NULL)
{
// 打开成功,映射对象的一个视图,得到指向共享内存的指针,显示出里面的数据
pBuffer = ::MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
printf(" 读出共享内存数据:“%s”\n", (char*)pBuffer);
}
else
{
// 打开失败,创建之
hMap = ::CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
strlen(szData) + 1,
szName);
// 映射对象的一个视图,得到指向共享内存的指针,设置里面的数据
pBuffer = ::MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
strcpy((char*)pBuffer, szData);
printf(" 写入共享内存数据:“%s”\n", (char*)pBuffer);
}
getchar();
// 解除文件映射,关闭内存映射文件对象句柄
::UnmapViewOfFile(pBuffer);
::CloseHandle(hMap);
return;
}
封装共享内存类CShareMemory
为了使用方便,本书将共享内存的功能封装到了CShareMemory类中,代码如下。
// ShareMemory.h文件
#ifndef __SHAREMEMORY_H__
#define __SHAREMEMORY_H__
class CShareMemory
{
public:
// 构造函数和析构函数
CShareMemory(const char * pszMapName, int nFileSize = 0, BOOL bServer = FALSE);
~CShareMemory();
// 属性
LPVOID GetBuffer() const { return m_pBuffer; }
// 实现
private:
HANDLE m_hFileMap;
LPVOID m_pBuffer;
};
inline CShareMemory::CShareMemory(const char * pszMapName,
int nFileSize, BOOL bServer) : m_hFileMap(NULL), m_pBuffer(NULL)
{
if(bServer)
{
// 创建一个内存映射文件对象
m_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL, PAGE_READWRITE, 0, nFileSize, pszMapName);
}
else
{
// 打开一个内存映射文件对象
m_hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, pszMapName);
}
// 映射它到内存,取得共享内存的首地址
m_pBuffer = (LPBYTE)MapViewOfFile(
m_hFileMap,
FILE_MAP_ALL_ACCESS,
0,
0,
0
);
}
inline CShareMemory::~CShareMemory()
{
// 取消文件的映射,关闭文件映射对象句柄
UnmapViewOfFile(m_pBuffer);
CloseHandle(m_hFileMap);
}
#endif // __SHAREMEMORY_H__
共享内存的主要作用就是进程间通信。通信的双方一个是服务器方,一个是客户方。服务器方创建一个命名的内存映射文件对象,客户方打开这个对象。双方在MapViewOfFile函数返回的共享内存中交流数据。
CShareMemory类就是根据这一原则而设计的。服务器方在创建CShareMemory对象时必须指定文件的大小(共享内存的大小),并将bServer参数设为TRUE;客户方仅指定对象名称就行了。类的惟一接口函数GetBuffer返回指向共享内存的指针。