C# 用内存映射文件读取大文件(.txt)
网上有好多这类的文章,大部分都是用C/C++写的,也有部分C#写的,都思想都是一样的,调用win32 API。
至于什么是内存映射文件,相信还是有好多人不知道是怎么一回事的,我也是偶然看window 核心编程了解到的。
C# 读取大文件的方法也是用的用StreamReader一次读出来,再用MemoryStream放在内存,再用StreamReader一行行的读出来,速度也挺快的,16M的文本大概也就8秒左右,算起来差不多算快了。不过还是不能满足大文件(我没测试)。
string content = string.Empty;
using (StreamReader sr = new StreamReader(op.FileName))
{
content = sr.ReadToEnd();//一次性读入内存
}
MemoryStream ms = new MemoryStream(Encoding.GetEncoding("GB2312").GetBytes(content));//放入内存流,以便逐行读取
long line = 0;
using (StreamReader sr = new StreamReader(ms))
{
while (sr.Peek() > -1)
{
this.richTextBox1.AppendText(sr.ReadLine() + "\r\n");
Application.DoEvents();
}
}
内存映射文件概述
内存文件映射也是Windows的一种内存管理方法,提供了一个统一的内存管理特征,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问。通过文件映射这种使磁盘文件的全部或部分内容与进程虚拟地址空间的某个区域建立映射关联的能力,可以直接对被映射的文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。
在使用内存映射文件进行I/O处理时,系统对数据的传输按页面来进行。至于内部的所有内存页面则是由虚拟内存管理器来负责管理,由其来决定内存页面何时被分页到磁盘,哪些页面应该被释放以便为其它进程提供空闲空间,以及每个进程可以拥有超出实际分配物理内存之外的多少个页面空间等等。由于虚拟内存管理器是以一种统一的方式来处理所有磁盘I/O的(以页面为单位对内存数据进行读写),因此这种优化使其有能力以足够快的速度来处理内存操作。
使用内存映射文件时所进行的任何实际I/O交互都是在内存中进行并以标准的内存地址形式来访问。磁盘的周期性分页也是由操作系统在后台隐蔽实现的,对应用程序而言是完全透明的。内存映射文件的这种特性在进行大文件的磁盘事务操作时将获得很高的效益。
需要说明的是,在系统的正常的分页操作过程中,内存映射文件并非一成不变的,它将被定期更新。如果系统要使用的页面目前正被某个内存映射文件所占用,系统将释放此页面,如果页面数据尚未保存,系统将在释放页面之前自动完成页面数据到磁盘的写入。
对于使用页虚拟存储管理的Windows操作系统,内存映射文件是其内部已有的内存管理组件的一个扩充。由可执行代码页面和数据页面组成的应用程序可根据需要由操作系统来将这些页面换进或换出内存。如果内存中的某个页面不再需要,操作系统将撤消此页面原拥用者对它的控制权,并释放该页面以供其它进程使用。只有在该页面再次成为需求页面时,才会从磁盘上的可执行文件重新读入内存。同样地,当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态、动态数据,一旦对它们的操作被提交,这些页面也将被备份至系统的页面文件,这与可执行文件被用来备份执行代码页面的过程是很类似的。图1展示了代码页面和数据页面在磁盘存储器上的备份过程:
显然,如果可以采取同一种方式来处理代码和数据页面,无疑将会提高程序的执行效率,而内存映射文件的使用恰恰可以满足此需求。
对大文件的管理
内存映射文件对象在关闭对象之前并没有必要撤销内存映射文件的所有视图。在对象被释放之前,所有的脏页面将自动写入磁盘。通过 CloseHandle()关闭内存映射文件对象,只是释放该对象,如果内存映射文件代表的是磁盘文件,那么还需要调用标准文件I/O函数来将其关闭。在处理大文件处理时,内存映射文件将表示出卓越的优势,只需要消耗极少的物理资源,对系统的影响微乎其微。下面先给出内存映射文件的一般编程流程框图:
图2 使用内存映射文件的一般流程
而在某些特殊行业,经常要面对十几GB乃至几十GB容量的巨型文件,而一个32位进程所拥有的虚拟地址空间只有232 = 4GB,显然不能一次将文件映像全部映射进来。对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。这需要对上面的一般流程进行适当的更改:
1)映射文件开头的映像。
2)对该映像进行访问。
3)取消此映像
4)映射一个从文件中的一个更深的位移开始的新映像。
5)重复步骤2,直到访问完全部的文件数据。
下面是用C#写的代码,大部分代码转自网上,自己在原来的基础上改了一改。
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;
namespace TestOpenFileMap
{
public class FileMap
{
[StructLayout(LayoutKind.Sequential)]
internal struct SYSTEM_INFO
{
public uint dwOemId;
public uint dwPageSize;
public uint lpMinimumApplicationAddress;
public uint lpMaximumApplicationAddress;
public uint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public uint dwProcessorLevel;
public uint dwProcessorRevision;
}
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE =0x40000000;
private const int OPEN_EXISTING = 3;
private const int INVALID_HANDLE_VALUE = -1;
private const int FILE_ATTRIBUTE_NORMAL = 0x80;
private const uint FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000;
private const uint PAGE_READWRITE = 0x04;
private const int FILE_MAP_COPY = 1;
private const int FILE_MAP_WRITE = 2;
private const int FILE_MAP_READ = 4;
/// <summary>
/// 内存映射文件句柄
/// </summary>
/// <param name="hFile"></param>
/// <param name="lpFileMappingAttributes"></param>
/// <param name="flProtect"></param>
/// <param name="dwMaximumSizeHigh"></param>
/// <param name="dwMaximumSizeLow"></param>
/// <param name="lpName"></param>
/// <returns></returns>
[DllImport("kernel32.dll")]
internal static extern IntPtr CreateFileMapping(IntPtr hFile,
IntPtr lpFileMappingAttributes, uint flProtect,
uint dwMaximumSizeHigh,
uint dwMaximumSizeLow, string lpName);
/// <summary>
/// 内存映射文件
/// </summary>
/// <param name="hFileMappingObject"></param>
/// <param name="dwDesiredAccess"></param>
/// <param name="dwFileOffsetHigh"></param>
/// <param name="dwFileOffsetLow"></param>
/// <param name="dwNumberOfBytesToMap"></param>
/// <returns></returns>
[DllImport("kernel32.dll")]
internal static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint
dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow,
uint dwNumberOfBytesToMap);
/// <summary>
/// 撤消文件映像
/// </summary>
/// <param name="lpBaseAddress"></param>
/// <returns></returns>
[DllImport("kernel32.dll")]
internal static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
/// <summary>
/// 关闭内核对象句柄
/// </summary>
/// <param name="hObject"></param>
/// <returns></returns>
[DllImport("kernel32.dll")]
internal static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// 打开要映射的文件
/// </summary>
/// <param name="lpFileName"></param>
/// <param name="dwDesiredAccess"></param>
/// <param name="dwShareMode"></param>
/// <param name="securityAttrs"></param>
/// <param name="dwCreationDisposition"></param>
/// <param name="dwFlagsAndAttributes"></param>
/// <param name="hTemplateFile"></param>
/// <returns></returns>
[DllImport("kernel32.dll")]
internal static extern IntPtr CreateFile(string lpFileName,
uint dwDesiredAccess, FileShare dwShareMode, IntPtr securityAttrs,
FileMode dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
/// <summary>
/// 得到文件大小
/// </summary>
/// <param name="hFile"></param>
/// <param name="highSize"></param>
/// <returns></returns>
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern uint GetFileSize(IntPtr hFile, out uint highSize);
/// <summary>
/// 得到系统信息
/// </summary>
/// <param name="lpSystemInfo"></param>
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern void GetSystemInfo(ref SYSTEM_INFO lpSystemInfo);
/// <summary>
/// 使用内存文件映射得到文件内容
/// </summary>
/// <param name="path">文件路径</param>
/// <returns></returns>
public StringBuilder GetFileContent(string path)
{
StringBuilder sb = new StringBuilder();
IntPtr fileHandle = CreateFile(path,
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)fileHandle)
{
IntPtr mappingFileHandle = CreateFileMapping(
fileHandle, IntPtr.Zero, PAGE_READWRITE, 0, 0, "~MappingTemp");
if (mappingFileHandle != IntPtr.Zero)
{
SYSTEM_INFO systemInfo = new SYSTEM_INFO(); ;
GetSystemInfo(ref systemInfo);
//得到系统页分配粒度
uint allocationGranularity = systemInfo.dwAllocationGranularity;
uint fileSizeHigh=0;
//get file size
uint fileSize = GetFileSize(fileHandle, out fileSizeHigh);
fileSize |= (((uint)fileSizeHigh) << 32);
//关闭文件句柄
CloseHandle(fileHandle);
uint fileOffset = 0;
uint blockBytes = 1000 * allocationGranularity;
if (fileSize < 1000 * allocationGranularity)
blockBytes = fileSize;
//分块读取内存,适用于几G的文件
while (fileSize > 0)
{
// 映射视图,得到地址
IntPtr lpbMapAddress = MapViewOfFile(mappingFileHandle, FILE_MAP_COPY | FILE_MAP_READ | FILE_MAP_WRITE,
(uint)(fileOffset >> 32), (uint)(fileOffset & 0xFFFFFFFF),
blockBytes);
if (lpbMapAddress == IntPtr.Zero)
{
return sb;
}
// 对映射的视图进行访问
byte[] temp = new byte[blockBytes];
//从非托管的内存中复制内容到托管的内存中
Marshal.Copy(lpbMapAddress, temp, 0, (int)blockBytes);
//用循环太慢了,文件有几M的时候就慢的要死,还是用上面的方法直接
//for (uint i = 0; i < dwBlockBytes; i++)
//{
// byte vTemp = Marshal.ReadByte((IntPtr)((int)lpbMapAddress + i));
// temp[i] = vTemp;
//}
//此时用ASCII解码比较快,但有中文会有乱码,用gb2312即ANSI编码也比较快,16M的文件大概4秒就读出来了
//但用unicode解码,文件大的时候会非常慢,会现卡死的状态,不知道为什么?
//ASCIIEncoding encoding = new ASCIIEncoding();
//System.Text.UnicodeEncoding encoding = new UnicodeEncoding();
//sb.Append(encoding.GetString(temp));
sb.Append(System.Text.Encoding.GetEncoding("gb2312").GetString(temp));
// 撤消文件映像
UnmapViewOfFile(lpbMapAddress);
// 修正参数
fileOffset += blockBytes;
fileSize -= blockBytes;
}
//close file mapping handle
CloseHandle(mappingFileHandle);
}
}
return sb;
}
}
}
经过测试16M的文本4秒可以读出来。
现在是有两个问题还没有解决:
1.就是编码的问题,用Unicode解码的时候,文件大会很慢,而用ANSI和ASCII就很快。不知道为什么,望知情者告之。
2.怎么知道文件的编码是什么?用win32 IsTestUnicode只能判断两种,而且还不保证是对。