網上有好多這類的文章,大部分都是用C/C++寫的,也有部分C#寫的,都思想都是一樣的,調用win32 API。
至於什么是內存映射文件,相信還是有好多人不知道是怎么一回事的,我也是偶然看window 核心編程了解到的。
C# 讀取大文件的方法也是用的用StreamReader一次讀出來,再用MemoryStream放在內存,再用StreamReader一行行的讀出來,速度也挺快的,16M的文本大概也就8秒左右,算起來差不多算快了。不過還是不能滿足大文件(我沒測試)。
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#寫的代碼,大部分代碼轉自網上,自己在原來的基礎上改了一改。

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只能判斷兩種,而且還不保證是對。