C#直接读取磁盘文件(类似linux的Direct IO模式)
由于项目需要测试windows下的IO性能,因此要写个小程序,按照要求读取磁盘上的文件。在读取文件的时候,测试Windows的IO性能。
主要内容:
- 程序的要求
- 一般的FileStream方式
- 利用kernel32.dll中的CreateFile函数
1. 程序的要求
程序的要求很简单。
(1)命令行程序
(2)有3个参数,读取的文件名,一次读取buffer size,读取的次数count
(3)如果读取次数count未到,文件已经读完,就再次从头读取文件。
使用格式如下:
C:\>****.exe “c:\****.bin” 32768 32768
读取文件“c:\****.bin”,每次读取4K,读取32768次,读取的量大概1G。
2. 一般的FileStream方式
利用FileStream来读取文件,非常简单,代码如下:
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Reflection; namespace DirectIO { public class DIOReader { static void Main(string[] args) { long start = DateTime.Now.Ticks; if (args.Length < 3) { Console.WriteLine("parameter error!!"); return; } FileStream input = null; try { int bs = Convert.ToInt32(args[1]); int count = Convert.ToInt32(args[2]); input = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.None, bs); byte[] b = new byte[bs]; for (int i = 0; i < count; i++) { if (input.Read(b, 0, bs) == 0) input.Seek(0, SeekOrigin.Begin); } Console.WriteLine("Read successed! "); Console.WriteLine(DateTime.Now.Ticks - start); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { if (input != null) { input.Flush(); input.Close(); // 清除使用的对象 GC.Collect(); GC.Collect(); } } } } }
编译后的exe文件可以按照既定要求执行,但是对于同一文件,第二次读取明显比第一次快很多(大家可以用个1G左右的大文件试试)。第三次读取,第四次读取……和第二次差不多,都很快。
基于上述情况,可以判断是缓存的原因,导致第二次及以后各次都比较快。
但是从代码中来看,已经执行了input.Flush();input.Close();甚至是GC.Collect();
所以可能是Windows系统或者CLR对文件读取操作进行了优化,使用了缓存。
3. 利用kernel32.dll中的CreateFile函数
既然上述方法行不通,就得调查新的方法。通过google的查询,大部分人都是建议用C/C++调用系统API来实现。
不过最后终于找到了用c#实现了无缓存直接读取磁盘上的文件的方法。其实也是通过DllImport利用了kernel32.dll,不完全是托管代码。(估计用纯托管代码实现不了)
参考的文章:How do I read a disk directly with .Net?
还有msdn中的CreateFile API
实现代码就是参考的How do I read a disk directly with .Net?,分为两部分
(1)利用CreateFile API构造的可直接读取磁盘的DeviceStream
using System; using System.Runtime.InteropServices; using System.IO; using Microsoft.Win32.SafeHandles; namespace DirectIO { public class DeviceStream : Stream, IDisposable { public const short FILE_ATTRIBUTE_NORMAL = 0x80; public const short INVALID_HANDLE_VALUE = -1; public const uint GENERIC_READ = 0x80000000; public const uint NO_BUFFERING = 0x20000000; public const uint GENERIC_WRITE = 0x40000000; public const uint CREATE_NEW = 1; public const uint CREATE_ALWAYS = 2; public const uint OPEN_EXISTING = 3; // Use interop to call the CreateFile function. // For more information about CreateFile, // see the unmanaged MSDN reference library. [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadFile( IntPtr hFile, // handle to file byte[] lpBuffer, // data buffer int nNumberOfBytesToRead, // number of bytes to read ref int lpNumberOfBytesRead, // number of bytes read IntPtr lpOverlapped // // ref OVERLAPPED lpOverlapped // overlapped buffer ); private SafeFileHandle handleValue = null; private FileStream _fs = null; public DeviceStream(string device) { Load(device); } private void Load(string Path) { if (string.IsNullOrEmpty(Path)) { throw new ArgumentNullException("Path"); } // Try to open the file. IntPtr ptr = CreateFile(Path, GENERIC_READ, 0, IntPtr.Zero, OPEN_EXISTING, NO_BUFFERING, IntPtr.Zero); handleValue = new SafeFileHandle(ptr, true); _fs = new FileStream(handleValue, FileAccess.Read); // If the handle is invalid, // get the last Win32 error // and throw a Win32Exception. if (handleValue.IsInvalid) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return false; } } public override void Flush() { return; } public override long Length { get { return -1; } } public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } /// <summary> /// </summary> /// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and /// (offset + count - 1) replaced by the bytes read from the current source. </param> /// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream. </param> /// <param name="count">The maximum number of bytes to be read from the current stream.</param> /// <returns></returns> public override int Read(byte[] buffer, int offset, int count) { int BytesRead = 0; var BufBytes = new byte[count]; if (!ReadFile(handleValue.DangerousGetHandle(), BufBytes, count, ref BytesRead, IntPtr.Zero)) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } for (int i = 0; i < BytesRead; i++) { buffer[offset + i] = BufBytes[i]; } return BytesRead; } public override int ReadByte() { int BytesRead = 0; var lpBuffer = new byte[1]; if (!ReadFile( handleValue.DangerousGetHandle(), // handle to file lpBuffer, // data buffer 1, // number of bytes to read ref BytesRead, // number of bytes read IntPtr.Zero )) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); ;} return lpBuffer[0]; } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override void Close() { handleValue.Close(); handleValue.Dispose(); handleValue = null; base.Close(); } private bool disposed = false; new void Dispose() { Dispose(true); base.Dispose(); GC.SuppressFinalize(this); } private new void Dispose(bool disposing) { // Check to see if Dispose has already been called. if (!this.disposed) { if (disposing) { if (handleValue != null) { _fs.Dispose(); handleValue.Close(); handleValue.Dispose(); handleValue = null; } } // Note disposing has been done. disposed = true; } } } }
注意和原文相比,改动了一个地方。即加了个NO_BUFFERING的参数,并在调用CreateFile时使用了这个参数。
IntPtr ptr = CreateFile(Path, GENERIC_READ, 0, IntPtr.Zero, OPEN_EXISTING, NO_BUFFERING, IntPtr.Zero);
之前没有加这个参数的时候,在xp上测试还是第二次比第一次快很多。
(2)完成指定要求的DIOReader
using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Reflection; namespace DirectIO { public class DIOReader { static void Main(string[] args) { long start = DateTime.Now.Ticks; if (args.Length < 3) { Console.WriteLine("parameter error!!"); return; } BinaryReader input = null; try { int bs = Convert.ToInt32(args[1]); int count = Convert.ToInt32(args[2]); input = new BinaryReader(new DeviceStream(args[0])); byte[] b = new byte[bs]; for (int i = 0; i < count; i++) { if (input.Read(b, 0, bs) == 0) input.BaseStream.Seek(0, SeekOrigin.Begin); } Console.WriteLine("Read successed! "); Console.WriteLine("Total cost " + (new TimeSpan(DateTime.Now.Ticks - start)).TotalSeconds + " seconds"); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { if (input != null) { input.Close(); } //Console.ReadKey(true); } } } }
这样,就完成了类似linux上Direct IO模式读取文件的操作。
通过这个例子可以看出,C#不仅可以开发上层的应用,也可以结合一些非托管的dll完成更加底层的操作。