from: http://hi.baidu.com/zhezhe101/item/2c3d01fafa1312c30dd1c874
最近帮朋友做了一个处理文件格式的程序,一开始用的是普通的文件流读写技术,做了好几天,怎么做怎么优化都达不到理想的速度,每次转换完都要花上五六分钟,由于文件内容很大,后来网上一查,利用内存映射文件技术可以达到想要的效果,代码改了后一测试,确实是快了很多,一般四五秒就搞定,网上的都是MFC的资料,虽然还是有人整出了C#版的,但还是借用API的东西,C++就是王啊,当初微软力推C#,把C#搞成Win版和Web版,还有手机版的,花这么大精力,为什么不把C++扩展的那么强大呢,Win的程序,C++是各大小公司的首先,Web
C++ 是空白,人家都用java 去了,C#说的那么好,在我看来不如C++一半,其实微软自己的程序都离开不了C++去做,如果哪天微软能把Web C++ 搞上去,那java的市场又要划分一半出来了。
1.概览
(1)什么是内存映射文件
内存映射文件是由一个文件到一块内存的映射,使进程虚拟地址空间的某个区域与磁盘上某个文件的部分或全部内容的建立映射。
建立映射后,通过该区域可以直接对被映射的磁盘文件进行访问.而不必执行文件I/O操作也无需对文件内容进行缓冲处理。
就好像整个被映射的文件都加载到了内存一样,因此内存文件映射非常适合于用来管理大文件。
注:与虚拟内存使用的是Page file不同,内存映射使用的是磁盘上的用户指定的文件。
内存映射文件并不是简单的文件I/O操作,实际用到了Windows的核心编程技术--内存管理。所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识。
(2)用途
系统用内存映射文件加载和执行EXE,DLL文件。既节省了Page file的空间,又加快了程序的执行。
用内存映射文件机制访问文件遮蔽了对文件I/O操作和文件内容的缓存操作,它是最有效的进程间通信机制,其它的进程间通信机制都是基于内存映射文件的。
这种函数最适用于需要读取文件并且对文件内包含的信息做语法分析的应用程序,如对输入文件进行语法分析的彩色语法编辑器,编译器等。把文件映射后进行读和分析,能让应用程序使用内存操作来操纵文件,而不必
在文件里来回地读、写、移动文件指针。
有些操作,如放弃“读”一个字符,在以前是相当复杂的,用户需要处理缓冲区的刷新问题。在引入了映射文件之后,就简单的多了。应用程序要做的只是使指针减少一个值。
映射文件的另一个重要应用就是用来支持永久命名的共享内存。要在两个应用程序之间共享内存,可以在一个应用程序中创建一个文件并映射之,然后另一个应用程序可以通过打开和映射此文件把它作为共享的内存来使
用。
实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
2.内存映射文件的用法
使用前:
(1)创建一个文件内核对象,指向磁盘上要做为内存映射的文件
(2)创建一个文件映射内核对象,并告诉系统文件的大小及如何访问这个文件(读、写)
(3)告诉系统把文件映射对象的部分和全部映射到进程的地址空间中。
使用后
(1)告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像
(2)关闭文件映射内核对象
(3)关闭文件内核对象。
3.内存映射与一致性
1.仅使用内存映射
多个进程可以同时对一个文件进行映射。当其中一个进程修改文件的内容时,被修改的内容会同时反映到其它进程中。这是因为文件在内存中只有一份实例,进程只是对这块内存做了映射并没有创建副本。
2.内存映射与WriteFile操作
若一个进程A用内存映射对文件进行操作,另一个进程B用文件操作函数WriteFile对同一文件进行操作,A对文件的修改不会反应到B中,反之亦然。因为内存映射永远也不要在网络文件中使用。在本地可以通过独占打开文件避
名文件操作的不一致性。
4.Common API:
CreateFileMapping MapViewOfFile OpenFileMapping
UnmapViewOfFile FlushViewOfFile、
5、相关函数说明
一、CreateFileMapping 为指定文件创建一个有名或无名的文件映象;
HANDLE CreateFileMapping
(
HANDLE hFile, // 映射文件的句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全描述符指针
DWORD flProtect, // 对映射对象的保护
DWORD dwMaximumSizeHigh, // 对象最大长度的高32位
DWORD dwMaximumSizeLow, // 对象最大长度的低32位
LPCTSTR lpName // 文件内存映射对象的名字
);
注意:
hFile:映射文件的句柄,文件的打开模式必须与flProtect参数指定的相一致;如果这个参数值为0xFFFFFFFF,那 么必须在dwMaximumSizeHigh和dwMaximumSizeLow参数中指定映射对象的大小。并且将在操作系统虚拟内存页
面替换文件中创建 文件映射对象,而不是使用磁盘文件,同时必须给出这个映射对象的大小。文件映射对象通过副本,遗传或名字来共享。
lpFileMappingAttributes:安全描述符指针,决定返回句柄是否能被子进程继承,如果是NULL,那么子进程不能继承。WinNt中,如果是NULL,那么文件映射对象得到一个默认的安全描述符。
flProtect:为得到的文件试图指定保护模式,可以被设置为下列值:
PAGE_READONLY :只读属性,并且hFile对应的文件必须以GENERIC_READ形式打开。
PAGE_READWRITE:可读可写属性,并且hFile对应的文件必须以GENERIC_READ 和 GENERIC_WRITE形式打开。
PAGE_WRITECOPY:对可写区域复制后操作,并且hFile对应的文件必须以GENERIC_READ 和 GENERIC_WRITE形式打开。
dwMaximumSizeHigh,dwMaximumSizeLow:如果这两个参数为0,则文件映射对象的最大长度等于hFile指定的文件长度。
lpName: 文件映射对象的名字,如果这个名字已存在,则按照flProtect指定的来处理映射对象。如果此参数为空,则创建一个无名字的文件映射对象。如果此参数 的名字与系统事件的名字相同,则函数执行失败,
GetLastError返回 ERROR_INVALID_HANDLE;
返回值:函数调用成功返回文件映射对象的句柄,如果文件映射对象已经存在则返回原有映射对象的句柄,GetLastError返回ERROR_ALREADY_EXISTS。函数执行失败返回Null。
二、FlushViewOfFile 把文件映射视图中的修改的内容或全部写回到磁盘文件中
BOOL FlushViewOfFile
(
LPCVOID lpBaseAddress, // 修改内容的起始地址
DWORD dwNumberOfBytesToFlush // 修改的字节数目
);
返回值:函数执行成功返回非零。
三、MapViewOfFile 在调用进程的地址空间映射一个文件视图
LPVOID MapViewOfFile
(
HANDLE hFileMappingObject, // 已创建的文件映射对象句柄
DWORD dwDesiredAccess, // 访问模式
DWORD dwFileOffsetHigh, // 文件偏移的高32位
DWORD dwFileOffsetLow, // 文件偏移的低32位
DWORD dwNumberOfBytesToMap // 映射视图的大小
);
注意:
hFileMappingObject: 由CreateFileMapping 或 OpenFileMapping 返回的文件映射对象句柄。
dwDesiredAccess:映射视图的访问模式,与创建文件映射对象的保护模式flProtect有关,可以被设置为下列值:
FILE_MAP_WRITE:一个可读写属性的文件视图被创建,保护模式为PAGE_READWRITE
FILE_MAP_READ :一个只读属性的文件视图被创建,保护模式为PAGE_READWRITE 或 PAGE_READONLY
FILE_MAP_ALL_ACCESS:与FILE_MAP_WRITE模式相同
FILE_MAP_COPY:保护模式为PAGE_WRITECOPY时,得到一个视图文件,当你对视图文件写操作时,页面自动交换,并且你所做的修改不会损坏原始数据资料。
dwNumberOfBytesToMap:映射文件部分的大小,如果为0,则映射整个文件。
返回值:如果成功返回返回映射视图的起始地址,如果失败返回NULL。
用MapViewOfFile处理大文件时,如果文件过大,如400M,则无法一次性映射入内存,否则会出现1132错误,即内存不足。原因可能为操作系统无法找到连续的内存。因此需要通过分页的方式,逐页将文件内容映射到内存。
MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;
参数dwNumberOfBytesToMap指定了数据文件的映射长度,这里需要特别指出的是,对于Windows 9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);但是在Windows 2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。
由此看出,分页映射文件时,每页的起始位置startpos,必须为64K的整数倍。
四、MapViewOfFileEx 在调用进程的地址空间映射一个文件视图,并且允许调用进程为映射视图指定特殊的内存地址
LPVOID MapViewOfFileEx
(
HANDLE hFileMappingObject, // 文件映射对象的句柄
DWORD dwDesiredAccess, // 访问模式
DWORD dwFileOffsetHigh, // 文件偏移的高32位
DWORD dwFileOffsetLow, // 文件偏移的低32位
DWORD dwNumberOfBytesToMap, // 映射视图的大小
LPVOID lpBaseAddress // 指定映射视图的其实内存地址
);
注意:与MapViewOfFile用法相同,但是如果指定的内存地址空间大小不够,则函数执行失败。
五、OpenFileMapping 打开一个已命名的文件映射对象
HANDLE OpenFileMapping
(
DWORD dwDesiredAccess, // 访问模式
BOOL bInheritHandle, // 继承标志
LPCTSTR lpName // 文件映射对象名指针
);
注意:
dwDesiredAccess:访问模式与MapViewOfFile中的访问模式相同。
bInheritHandle:继承标志,是否可以被一个新的进程继承使用,如果为TRUE,就可以被一个新进程继承句柄。
返回值:成功返回一个已命名的文件映射对象,失败返回NULL。
六、UnmapViewOfFile 删除文件的映射视图
BOOL UnmapViewOfFile
(
LPCVOID lpBaseAddress // 映射视图起始地址
);
注意:
lpBaseAddress:映射视图起始地址,由 MapViewOfFile 函数 MapViewOfFileEx产生。
返回值:如果调用成功返回非零,并且所有指定地址内的脏页面会被写入硬盘。调用失败返回零。
七、例子
using System;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
using System.Windows.Forms; //请先添加System.Windows.Forms引用
namespace 内存映射操作大文本
{
class Program
{
[DllImport("kernel32.dll")]
public static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes, uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow, string lpName);
[DllImport("kernel32.dll")]
public static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint
dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow,
IntPtr dwNumberOfBytesToMap);
[DllImport("kernel32.dll")]
public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateFile(string lpFileName,
int dwDesiredAccess, FileShare dwShareMode, IntPtr securityAttrs,
FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32.dll")]
public static extern uint GetFileSize(IntPtr hFile, IntPtr lpFileSizeHigh);
public const int GENERIC_READ = -2147483648; //0x80000000
public const int GENERIC_WRITE = 0x40000000;
public const int GENERIC_EXECUTE = 0x20000000;
public const int GENERIC_ALL = 0x10000000;
public const int FILE_ATTRIBUTE_NORMAL = 0x80;
public const int FILE_FLAG_SEQUENTIAL_SCAN = 0x8000000;
public const int INVALID_HANDLE_VALUE = -1;
public const int PAGE_NOACCESS = 1;
public const int PAGE_READONLY = 2;
public const int PAGE_READWRITE = 4;
public const int FILE_MAP_COPY = 1;
public const int FILE_MAP_WRITE = 2;
public const int FILE_MAP_READ = 4;
public static void getFile(string filName)
{
//用CreateFile打开您想要映射的文件,并获得一个文件的句柄
IntPtr vFileHandle = CreateFile(filName, GENERIC_READ | GENERIC_WRITE, FileShare.Read | FileShare.Write, IntPtr.Zero, FileMode.Open, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
IntPtr.Zero);
if (INVALID_HANDLE_VALUE != (int)vFileHandle)
{
//用CreateFileMapping创建一个内存映射对象,并获得一个文件的句柄
IntPtr vMappingHandle = CreateFileMapping(vFileHandle, IntPtr.Zero, PAGE_READWRITE, 0, 0, "~MappingTemp");
if (vMappingHandle != IntPtr.Zero)
{
//用MapViewOfFile将文件的全部或部分映射进内存。 注意映射視圖不應該太大,採取多次進行,並獲得視圖內存地址,然後就可以針對這段視圖進行操作了,操作完成,使用UnMapViewOfFile來清除資源.
IntPtr vHead = MapViewOfFile(vMappingHandle, FILE_MAP_COPY | FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, IntPtr.Zero);
if (vHead != IntPtr.Zero)
{
uint vSize = GetFileSize(vFileHandle, IntPtr.Zero);
//******************** A ***********************反文件内容*****************************************
//for (int i = 0; i <= vSize / 2; i++) //vSize如果不除以2,就会出现尝试读取受保护的内存出错
//{
// byte vTemp = Marshal.ReadByte((IntPtr)((int)vHead + i));
// Marshal.WriteByte((IntPtr)((int)vHead + i),
// Marshal.ReadByte((IntPtr)((int)vHead + vSize - i - 1)));
// Marshal.WriteByte((IntPtr)((int)vHead + vSize - i - 1), vTemp);
//}
//***************************************************** The End ***************************************
byte[] contentbyte = new byte[vSize];
for (int i = 0; i < vSize; i++)
{
byte vTemp = Marshal.ReadByte((IntPtr)((int)vHead + i),2);
contentbyte[i] = vTemp;
}
//******************** B ****************在消息框输出文件的内容************************************
//Encoding currEncoding = Encoding.Default;
//MessageBox.Show(currEncoding.GetString(contentbyte));
//****************************************************** The End ********************************************
//******************** C ****************关于编码转换 *********************************************
ASCIIEncoding encoding = new ASCIIEncoding();
MessageBox.Show(encoding.GetString(contentbyte));
Encoding ascii = Encoding.ASCII;
Encoding unicode = Encoding.Unicode;
//这里是将ASCII转换Unicode
byte[] unicodeBytes = Encoding.Convert(ascii, unicode, contentbyte); //将整个字节数组从一种编码转换为另一种编码。第一个参数是源编码格式,第二个参数是目标编码格式
string content = FromUnicodeByteArray(unicodeBytes);
//string content = Convert.ToBase64String(unicodeBytes);
MessageBox.Show(content);
//****************************************************** The End ********************************************
UnmapViewOfFile(vHead); //用UnmapViewOfFile来解除文件映射
}
CloseHandle(vMappingHandle); //用CloseHandle来关闭内存映射文件
}
CloseHandle(vFileHandle); //用CloseHandle来关闭文件
}
}
// 将一个包含ASCII编码字符的Byte数组转化为一个完整的String,可以使用如下的方法:
public static string FromASCIIByteArray(byte[] characters)
{
ASCIIEncoding encoding = new ASCIIEncoding( );
string constructedString = encoding.GetString(characters);
//或 string[] times = ASCIIEncoding.Default.GetString(buff).Split(',');
return (constructedString);
}
// 将一个包含Unicode编码字符的Byte数组转化为一个完整的String,可以使用如下的方法:
public static string FromUnicodeByteArray(byte[] characters)
{
UnicodeEncoding encoding = new UnicodeEncoding( );
string constructedString = encoding.GetString(characters);
return (constructedString);
}
static void Main(string[] args)
{
Console.Title = "内存映射操作大文本测试.....";
Console.ForegroundColor = ConsoleColor.Red;
Console.BackgroundColor = ConsoleColor.Green;
string filName;
if (args.Length > 0)
{
filName = args[0];
}
else
{
return;
}
Console.WriteLine("当前时间:{0}", DateTime.Now);
getFile(filName);
Console.WriteLine("完成后时间:{0}", DateTime.Now);
Console.ReadKey();
}
}
}
/*
StreamReader读入中文乱码
解决方法:
StreamReader sr=new StreamReader(filename,System.Text.Encoding.Default)
究其原因,原来自从Windows 2000之后的操作系统在文件处理时默认编码采用Unicode,所以.Net 的文件默认编码也是Unicode。除非另外指定,StreamReader 的默认编码为 Unicode,而不是当前系统的 ANSI 代码页。但是
文档大部分还是以ANSI编码储存,中文文本使用的是gb2312,所以才造成中文乱码的状况,也就是说在读取文本的时候要指定编码格式。
*/