由于项目需要测试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完成更加底层的操作。