65.9K
CodeProject 正在变化。 阅读更多。
Home

Dokan.Mem,一个文件系统原型

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (29投票s)

2010 年 7 月 20 日

CPOL

7分钟阅读

viewsIcon

147994

downloadIcon

4614

使用 Dokan 库和 C# 构建(几乎)一个 RAM 磁盘

Dokan.Mem1.png

引言

Hiroki Asakawa 创建了一个设备驱动程序,它允许用户模式下的应用程序模拟一个文件系统,并以 MIT 风格的许可证分发。本文展示了如何在 C# 中使用它来创建 RAM 磁盘的功能,并提供了一些关于构建自己的文件系统应用程序的技巧。

在运行代码之前,您必须先运行安装程序来安装一个代理驱动程序(dokan.sys)。这个驱动程序将作为内核和我们的自定义 .NET 解决方案之间的中间件。代理驱动程序的安装程序也包含在源代码中,但我建议从原始网站下载软件包,因为它们包含两个很酷的示例;第一个实现了 C 盘的镜像,第二个将注册表公开为一个只读驱动器。本文简要解释了 Dokan 库如何用于构建一个在功能上类似于 RAM 磁盘的应用程序。

将数据暴露为文件系统具有多种优势。它使您的数据能够立即被其他应用程序访问,而无需复杂的 UI。

在此处 [^] 下载基础安装程序和 Dokan 的 .NET 绑定。

Using the Code

以管理员身份登录,运行安装程序(仅 513 KB),打开项目并按 F5;此时应该会出现一个托盘图标,双击它应该会打开 Windows 资源管理器,指向一个模拟的硬盘。它将挂载在第一个可用的驱动器号上。将一个 zip 文件粘贴到那里并解压缩。:)

我选择实现一个基本的 RAM 磁盘来测试该库(版本 0.5.3),并将结果记录在此处。

解决方案中有三个项目

  • DokanNet - 这是 Dokan 库的 .NET 绑定
  • Buffers - 用于替换 MemoryStream
  • Dokan.Mem - Dokan 接口的示例实现,模拟 RAM 磁盘

DokanNet

本项目包含 Dokan 库的绑定,由 DokanOperations 接口提供。它对应用程序(如 Word)在文件系统上可以执行的操作进行了相当直接的定义。

  1  public interface DokanOperations
  2  {
  3      int CreateFile(string filename, ..., DokanFileInfo info);
  4      int OpenDirectory(string filename, DokanFileInfo info);
  5      int CreateDirectory(string filename, DokanFileInfo info);
  6      int Cleanup(string filename, DokanFileInfo info);
  7      int CloseFile(string filename, DokanFileInfo info);
  8      int ReadFile(string filename, ..., DokanFileInfo info);
  9      int WriteFile(string filename, ..., DokanFileInfo info);
 10      int FlushFileBuffers(string filename, DokanFileInfo info);
 11      int GetFileInformation(string filename, ..., DokanFileInfo info);
 12      int FindFiles(string filename, ArrayList files, DokanFileInfo info);
 13      int SetFileAttributes(string filename, ..., DokanFileInfo info);
 14      int SetFileTime(string filename, ..., DokanFileInfo info);
 15      int DeleteFile(string filename, DokanFileInfo info);
 16      int DeleteDirectory(string filename, DokanFileInfo info);
 17      int MoveFile(string filename, ..., DokanFileInfo info);
 18      int SetEndOfFile(string filename, long length, DokanFileInfo info);
 19      int SetAllocationSize(string filename, long length, DokanFileInfo info);
 20      int LockFile( string filename, long offset, long length, DokanFileInfo info);
 21      int UnlockFile(string filename, long offset, long length, DokanFileInfo info);
 22      int GetDiskFreeSpace(ref ulong freeBytesAvailable, ..., DokanFileInfo info);
 23      int Unmount(DokanFileInfo info);
 24  }

一旦您有了一个基于此接口的类(例如 'MyDokanOperations'),您就可以通过调用 Dokan 的主方法来启动新驱动器。只要此(阻塞)任务正在运行,驱动器就会可用。

下面提供了一个骨架应用程序,在 MyDokanOperations 类中实现了 DokanOperations。请注意,DokanOperations 是一个接口!它可能不命名为 IDokanOperations,但实际上它就是这样被理解的。

  1  class MyDokanOperations : DokanOperations
  2  {
  3     // implementation of DokanOperations interface goes here.. 
  4  }
  5  
  6  DokanOptions options = new DokanOptions
  7  {
  8  	DriveLetter = 'Z', 
  9  	DebugMode = true,     
 10  	UseStdErr = true,     
 11  	NetworkDrive = false, 
 12  	Removable = true,     // provides an "eject"-menu to unmount
 13  	UseKeepAlive = true,  // auto-unmount
 14  	ThreadCount = 0,      // 0 for default, 1 for debugging
 15  	VolumeLabel = "MyDokanDrive"
 16  };
 17  
 18  static void Main(string[] args)
 19  {
 20  	DokanNet.DokanMain(
 21  		options, 
 22  		new MyDokanOperations());
 23  }

所有示例都显示了一个控制台,这在开发新文件系统时很有用。它在调试时非常有用,您可以实际跟踪内核和模拟文件系统之间的交互。

Dokan.Mem

RAM 磁盘也显示控制台窗口,但通过将项目的输出类型更改为“Windows 应用程序”可以轻松禁用它。我提供了一个托盘图标,允许用户在发布模式下与应用程序进行交互。

回到那个 DokanOperations 接口;其中大多数调用都有实际的 API 对等项,您可以在 MSDN 上找到描述。API 描述非常有帮助,因为它记录了总体流程,并列出了它可能返回的错误代码。

让我们看一下 DeleteFile [^] 方法的实际实现;

  1  public int DeleteFile(string filename, DokanFileInfo info)
  2  {
  3  	// get parent folder
  4  	MemoryFolder parentFolder = _root.GetFolderByPath(
  5  		filename.GetPathPart());
  6  
  7  	// exists?
  8  	if (!parentFolder.Exists())
  9  		return -DokanNet.ERROR_PATH_NOT_FOUND;
 10  
 11  	// fetch file;
 12  	MemoryFile file = parentFolder.FetchFile(
 13  		filename.GetFilenamePart());
 14  
 15  	// exists?
 16  	if (!file.Exists())
 17  		return -DokanNet.ERROR_FILE_NOT_FOUND;
 18  
 19  	// delete it;
 20  	parentFolder.Children.Remove(file);
 21  	file.Content.Dispose();
 22  
 23  	return DokanNet.DOKAN_SUCCESS;
 24  }

MSDN 对 DeleteFile 函数的描述是

"如果应用程序尝试删除不存在的文件,DeleteFile 函数将失败并返回 ERROR_FILE_NOT_FOUND。如果文件是只读文件,该函数将失败并返回 ERROR_ACCESS_DENIED。"
RAM 磁盘会检查文件是否存在,但我还没有实现对文件属性的检查。至于设置文件属性,则根本没有实现;
  1  public int SetFileAttributes(
  2  	string filename,
  3  	FileAttributes attr,
  4  	DokanFileInfo info)
  5  {
  6  	return -DokanNet.DOKAN_ERROR;
  7  }

如果您再次查看接口,您会注意到没有 OpenFile 方法。如果系统想要打开一个文件,它会调用 CreateFile 方法。它接受与 CreateFile [^] API 使用的相同标志。根据 FileMode,我们创建或打开一个现有或不存在的文件

  1  public int CreateFile(
  2      string filename,
  3      FileAccess access,
  4      FileShare share,
  5      FileMode mode,
  6      FileOptions options,
  7      DokanFileInfo info)
  8  {
  9      [...] 
 10  
 11       // attempt to use the file
 12      switch (mode)
 13      {
 14          // Opens the file if it exists and seeks to the end of the file, 
 15          // or creates a new file
 16          case FileMode.Append:
 17              if (!thisFile.Exists())
 18                  MemoryFile.New(parentFolder, newName);
 19              return DokanNet.DOKAN_SUCCESS;
 20  
 21          // Specifies that the operating system should create a new file. 
 22          // If the file already exists, it will be overwritten. 
 23          case FileMode.Create:
 24              //if (!thisFile.Exists())
 25                  MemoryFile.New(parentFolder, newName);
 26              //else
 27              //	thisFile.Content = new Thought.Research.AweBuffer(1024); 
                 //MemoryStream();
 28              return DokanNet.DOKAN_SUCCESS;
 29  
 30          // Specifies that the operating system should create a new file. 
 31          // If the file already exists, an IOException is thrown.
 32          case FileMode.CreateNew:
 33              if (thisFile.Exists())
 34                  return -DokanNet.ERROR_ALREADY_EXISTS;
 35              MemoryFile.New(parentFolder, newName);
 36              return DokanNet.DOKAN_SUCCESS;
 37  
 38          // Specifies that the operating system should open an existing file. 
 39          // A System.IO.FileNotFoundException is thrown if the file does not exist.
 40          case FileMode.Open:
 41              if (!thisFile.Exists())
 42                  return -DokanNet.ERROR_FILE_NOT_FOUND;
 43              else
 44                  return DokanNet.DOKAN_SUCCESS;
 45  
 46          // Specifies that the operating system should open a file if it exists; 
 47          // otherwise, a new file should be created.
 48          case FileMode.OpenOrCreate:
 49              if (!thisFile.Exists())
 50                  MemoryFile.New(parentFolder, newName);
 51              return DokanNet.DOKAN_SUCCESS;
 52  
 53          // Specifies that the operating system should open an existing file. 
 54          // Once opened, the file should be truncated so that its size is zero bytes
 55          case FileMode.Truncate:
 56                  if (!thisFile.Exists())
 57                      thisFile = MemoryFile.New(parentFolder, newName);
 58                  thisFile.Size = 0;
 59                  return DokanNet.DOKAN_SUCCESS;
 60      }
 61  
 62      return DokanNet.DOKAN_ERROR;
 63  }

MemoryFolderMemoryFile 类用于在内存中以分层结构映射“文件”。有一个根节点代表驱动器的根,并且可能包含代表文件或文件夹的对象

Dokan.Mem2.png

您会注意到 MemoryFile 类是 abstract 的。

缓冲区 (Buffers)

RAM 磁盘原型最初基于 MemoryStream。这将使它成为一个“虚拟内存磁盘”,我们缺少一些功能才能称之为 RAM 磁盘。

我用一个缓冲区替换了 MemoryStream ,该缓冲区基于 David Pinch 的一个想法 [^]。他写了一个类,可用于分配内存,并在 Dispose 部分释放它。它最初设计用于提供保护已分配区域的能力,因此得名 ProtectedBuffer。这块内存可以作为流访问,缺点是它不能调整大小。

AweBuffer 类基于该 ProtectedBuffer 类,添加了一个对 VirtualLock [^] API 的调用。这样,信息就会被固定在物理内存中,只要线程正在运行*。没有它,RAM 磁盘的最大大小将仅受可用虚拟内存量的限制。理论上,它会加速数据访问。

实际上,RAM 磁盘倾向于将所有其他正在运行的应用程序推入交换文件,从而造成更大的延迟。这需要对不同的机器进行一些测试才能获得合理的指示,但我认为这个缓冲区比简单的 MemoryStream 性能更差。

*) 请参阅The Old New Thing [^] 上的 VirtualLock 条目

系统使用的是 MemoryStreamFile 还是 AweMemoryFile 是在应用程序启动时决定的。我认为会有更多的人想尝试其中的区别,所以很容易在 RAM 磁盘模式(使用 AweBuffer)和虚拟磁盘模式(使用 MemoryStream)之间切换。只需在 Dokan.Mem 项目的 Main 方法中切换 bool 值并重新编译即可。

  1  [STAThread()]
  2  static void Main(string[] args)
  3  {
  4      // Set us up a tray-icon to interact with the user;
  5      SetupNotifyIcon();
  6      
  7      // Set to false if you want to test with the AweBuffers 
  8      // instead of a MemoryStream
  9      MemoryFile.UseMemStream = true;

关注点

概述

  • 您需要以管理员身份登录才能运行文件系统
  • 提供的代码是原型质量的;没有锁定、没有错误处理,只有一个非常基础的 UI。
  • 在 Windows 中,路径和文件名不区分大小写;不同的应用程序在请求您的文件时会使用不同的 casing。
  • 没有重命名文件(或文件夹)的特殊方法,这是通过 MoveFile 方法完成的。
  • 文件夹通常按层级顺序排列,但这并非必需。可以完全省略文件夹结构,只显示文件(参见 FindFiles 方法)。
  • 您可能需要禁用任何杀毒软件,因为它们倾向于扫描每个文件。挂载后请求的第一个文件是“AutoRun.Inf”。
  • MemoryFile 类具有 FILE_ATTRIBUTE_NOT_CONTENT_INDEXED [^] 属性,以防止 Windows 对文件进行内容索引。
  • 在线 README [^] 包含对 Dokan 库内部结构及其工作原理更深入的解释。

如果您正在与 SQL Server 进行交互

因为能够通过 Explorer 在存储在 SQL Server 中的图片上使用 Paint.NET 是一件很酷的事情。

有 Stefan Delmarco 的 VarBinaryStream[^],提供了对 VarBinary 字段方便的基于流的访问。实现将按以下方式进行

  1  public int ReadFile(string sourcePath, byte[] buffer, ref uint readBytes,
  2      long offset, DokanFileInfo info)
  3  {
  4      using (var con = new SqlConnection())
  5      {
  6          int fileId = GetFileIdByPath(sourcePath);
  7          using (var myVarBinarySource = new VarBinarySource(
  8              con,
  9              "[TableName]", // source table
 10              "[Contents]",  // source column
 11              "[Id]",        // key column
 12              resourceId))
 13          {
 14              var dbStream = new VarBinaryStream(myVarBinarySource);
 15              dbStream.Seek(offset, SeekOrigin.Begin);
 16              readBytes = (uint) dbStream.Read(buffer, 0, buffer.Length);
 17          }
 18      }
 19      return DokanNet.DOKAN_SUCCESS;
 20  }

这会疯狂地打开和关闭数据库连接,但总的来说,它的性能相当不错。或者,您可以在 CreateFile 方法中打开连接,将其添加到列表中,并在调用 CloseFile 方法时再次关闭它。如果您需要测试数据;AdventureWorks 数据库包含一个名为 [Production].[Document] 的表,其中包含一些二进制形式的 Word 文档。

结论

Hiroki 做得很棒,Dokan 库的性能很棒。您可以随处放置断点,无需任何特殊设置即可逐步执行代码 :cool

至于 RAM 磁盘,它并没有带来太多速度提升。基准测试在尝试使用 AweBuffer 版本时崩溃,并且报告的文件创建速度仅为 7 MB/s。读取操作的最大速度约为 40 MB/s。作为比较,我的硬盘在创建文件时的速度约为 64 MB/s,平均读取速度为 165 MB/s。

历史

  • 初始版本,2010 年 7 月 20 日
© . All rights reserved.