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






4.86/5 (29投票s)
使用 Dokan 库和 C# 构建(几乎)一个 RAM 磁盘

引言
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 }
MemoryFolder
和 MemoryFile
类用于在内存中以分层结构映射“文件”。有一个根节点代表驱动器的根,并且可能包含代表文件或文件夹的对象

您会注意到 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 日