三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

手把手构建你的第一个Windows用户模式文件系统:WinFsp终极上手指南

手把手构建你的第一个Windows用户模式文件系统:WinFsp终极上手指南

手把手构建你的第一个Windows用户模式文件系统:WinFsp终极上手指南

【免费下载链接】winfspWindows File System Proxy - FUSE for Windows项目地址: https://gitcode.com/gh_mirrors/wi/winfsp

想象一个场景:你做了一个云盘客户端,想把云端目录"变"成本地盘符,让 Word、Photoshop 这类老软件直接读写;或者你想给数据仓库套一层虚拟目录,让同事通过\\server\share就能访问。第一个念头往往是"写个 Windows 内核文件系统驱动"——然后你就被劝退了:内核调试蓝屏、文档稀缺、一个指针错误就能让整台机器重启。

WinFsp(Windows File System Proxy,Windows 文件系统代理)正是为打破这堵墙而生的开源项目:它把"写文件系统"这件事从内核搬到了用户模式,让你像在 Linux 上用 FUSE(Filesystem in Userspace,用户空间文件系统)一样,用普通 C/C++ 程序就能造出一个真正的 Windows 盘符。下面我们就从一个问题出发:它到底是怎么把"内核级魔法"变成"普通程序"的?

你写的不是驱动,而是一个"应答服务"

先建立一个直觉。传统文件系统(如 NTFS)跑在内核里,你调用CreateFile时,整个 I/O 请求由内核直接处理。而 WinFsp 的思路是:内核里只留一个精简的"传话筒",真正的业务逻辑由你的用户态程序完成。

当你的程序调用CreateFile("X:\\hello.txt")时,Windows 内核会把这次调用打包成一个 IRP(I/O Request Packet,I/O 请求包),投递给 WinFsp 的内核驱动。驱动判断"这活儿得用户态文件系统来干",于是把 IRP 塞进一个特殊的 I/O 队列(I/O Queue),然后你的用户态文件系统通过一次特殊的DeviceIoControl调用(内部叫FSP_FSCTL_TRANSACT)把请求"取"出来处理,处理完再放回去,驱动完成收尾,CreateFile正常返回。

💡 一句话比喻:WinFsp 像一个"文件请求中转站",你的文件系统程序则是守在站台边随叫随到的搬运工。

整个过程里,你的程序只是被动应答——内核来一个请求,你回一个结果。这也解释了为什么"用户模式文件系统"在架构上完全可行:你不是在抢内核的活,而是在给内核当外援。

下面这张时序图(摘自项目文档 doc/WinFsp-as-an-IPC-Mechanism.asciidoc)展示了异步场景下的一次完整读写往返,OP是发起请求的应用,FS是你的文件系统程序:

一次文件操作的完整生命周期:应用发起 → 内核打包 IRP → 你的文件系统应答 → 结果原路返回

让"魔法"成立的三个关键设计

如果你只记住了 I/O 队列和 TRANSACT,其实已经抓住了 80% 的精髓。但有两个细节值得展开,它们决定了这个方案能不能快、能不能稳

零拷贝:数据不用"倒手"

性能问题是用户模式方案最容易被人诟病的地方。WinFsp 的应对方式是:在 IRP 进入_Prepare阶段时,把应用进程里的读写缓冲区直接映射到你的文件系统进程地址空间。也就是说,ReadFile要读的数据,你的程序可以直接在本地内存里读写,不需要在内核和用户态之间来回复制数据。一次请求只有两次进程上下文切换(请求去、响应回),这是整个 IPC 模型的固定成本。

队列即调度器:宁可"偏心",也要快

这是 WinFsp 作者在性能调优时的一个精彩故事(见 doc/Queued-Events.asciidoc):作者发现某项测试性能异常,用 xperf 跟踪后发现,罪魁祸首竟是 Windows 线程调度器的"公平原则"——内核试图让每个工作线程都有机会运行,导致频繁上下文切换。作者为此发明了 Queued Events(队列事件):用一个 KQUEUE(内核队列,即 IOCP 的底层结构)加自旋锁,模拟出"信号事件"的语义,却继承了 IOCP 的 LIFO(后进先出)等待纪律和并发线程数量限制。

💡 直觉解释:普通事件唤醒线程是"轮流排队",Queued Events 是"谁刚干完活还热乎,就让谁接着干",省掉了反复切换线程的开销。

内核驱动本身只有约一万行代码,却要覆盖磁盘型文件系统、网络型文件系统、安全描述符、重解析点、异步 I/O 等完整能力——这些细节都沉淀在 src/sys/(内核驱动)和 src/dll/(用户态 DLL,负责把内核请求翻译成你熟悉的回调函数)两个目录里。你不需要读懂它们,只需要知道:上层给你的是一个普通的结构体函数表

三步跑起你的第一个文件系统

理论说够了,动手。我们分三步:装环境、跑官方的内存文件系统 MEMFS、最后写一个 30 行的最小骨架。

第一步:安装带 Developer 组件的版本

git clone https://gitcode.com/gh_mirrors/wi/winfsp

安装器里务必勾选Developer选项——它才会带上示例文件系统、头文件和库文件,否则你只装了个运行时。

第二步:用 net use 挂载 MEMFS 验证环境

MEMFS 是一个纯内存文件系统,源码在 tst/memfs/,随安装器一并提供。启动后把它挂成盘符试试:

:: 把 MEMFS 挂载为 X 盘 net use X: \\memfs64\test :: 然后像普通磁盘一样使用它 X: echo hello world > hello.txt dir type hello.txt

如果dir能列出你刚创建的hello.txt,说明驱动、DLL、挂载链路全部打通了。挂载成功后,你在资源管理器里看到的界面大致是这样:

WinFsp 挂载的虚拟文件系统在资源管理器中和普通盘符毫无区别

第三步:看懂"文件系统 = 一张函数表"

以 tst/memfs/ 为例,MEMFS 的核心不过是填充一张FSP_FILE_SYSTEM_INTERFACE函数表——每个字段对应一种文件操作,你实现哪个,系统就有哪个功能:

// 摘自 tst/memfs/memfs.cpp(有删减):文件系统本质是一张回调函数表 static FSP_FILE_SYSTEM_INTERFACE MemfsInterface = { GetVolumeInfo, // 查询卷信息 SetVolumeLabel, // 设置卷标 GetSecurityByName, // 按名字查安全描述符 Create, // 创建文件/目录 Open, // 打开文件/目录 Overwrite, // 覆盖已有文件 Cleanup, // 清理(关闭前的最后机会) Close, // 关闭 Read, // 读数据 Write, // 写数据 GetFileInfo, // 查询文件属性 // ... 还有重命名、删除、枚举目录等 };

一个最小的文件系统骨架甚至可以只有 8 行(完整版见 doc/WinFsp-Tutorial.asciidoc 的 passthrough 教程):

#include <winfsp/winfsp.h> // 唯一需要的头文件 static NTSTATUS SvcStart(FSP_SERVICE *Service, ULONG argc, PWSTR *argv) { return STATUS_NOT_IMPLEMENTED; // 先返回"未实现",跑通链路 } int wmain(int argc, wchar_t **argv) { // 以"服务"方式运行:既能当控制台程序,也能被 WinFsp 启动器托管 return FspServiceRun(L"passthrough", SvcStart, 0, 0); }

编译时链接winfsp-x64.lib,运行后你会看到一个控制台窗口——它已经在等待内核投递文件操作了。按 Ctrl-C 退出;即使你强杀进程,WinFsp 也会自动清理卷设备和内核资源,不会蓝屏。这就是"用户模式"最大的安全感。

真实项目落地:三种 API 怎么选,性能怎么调

跑通 demo 之后,你会面临真实世界的选择题。WinFsp 提供三条 API 路线,建议如下:

API 路线位置适合谁典型理由
原生 WinFsp APIinc/winfsp/Windows 专属新项目功能最全,支持 ADS 数据流、重解析点、NTFS 级安全
FUSE API for Windowsinc/fuse/从 Linux 迁过来的 FUSE 文件系统改改路径就能在 Windows 编译运行
FUSE API for Cygwinopt/cygfuse/依赖 Cygwin 生态的老项目复用现有 POSIX 代码

📌 选型口诀:新项目用原生 API,老 FUSE 项目用 FUSE2/FUSE3 兼容层。如果你只是想把某个 Linux 文件系统"平移"过来,别重写,直接用 inc/fuse/ 里的兼容头文件。

性能:别被"用户模式"四个字吓到

项目自带完整的性能测试数据(doc/WinFsp-Performance-Testing.asciidoc),结论很反直觉:MEMFS 在多数文件操作上快于 NTFS,连缓存读写 I/O 都能和 NTFS 打平甚至略胜。下面这张图以 NTFS 为基准(=1.0),柱越短越快:

文件路径类操作性能对比(归一化到 NTFS=1,柱越短越快):MEMFS 全面领先 NTFS

性能调优的三个实用开关:

  1. 调整文件属性缓存:在FSP_FSCTL_VOLUME_PARAMS里设置FileInfoTimeout(如 1000ms),把频繁的GetFileInfo请求在驱动层直接命中缓存,省掉用户态往返。
  2. 减少不必要的回调:开启PostCleanupWhenModifiedOnly,只有文件被修改时才触发 Cleanup 回调,纯读场景少一半请求。
  3. 异步优先:对读多写少的场景,实现Read/Write时尽量快速返回、把重活放到工作线程池,让 WinFsp 的 I/O 队列有更多并行空间。

两个常见的坑

  • 响应必须及时:你的程序是系统组件,阻塞超过几秒,应用层就可能报"I/O 超时"。初始化完成后不要等待用户输入。
  • 崩溃不可怕,但要有序:进程被杀会自动清理,但如果你自己开了临时文件、网络连接,记得在Cleanup/SvcStop里关掉——WinFsp 只管它知道的资源。

从哪开始?三句话总结 + 一条行动路径

回顾这一路,你收获了三件东西:

  1. 一个心智模型:用户模式文件系统 = 内核把 I/O 请求打包成 IRP,你的程序通过 I/O 队列应答,WinFsp 负责中间所有内核脏活。
  2. 一套可复用的骨架FspServiceRun+ 一张FSP_FILE_SYSTEM_INTERFACE函数表,就是文件系统的全部入口。
  3. 一条性能与选型的判断线:新项目走原生 API,老项目走 FUSE 兼容层;用户模式不是性能原罪,缓存与队列设计才是。

下一步的行动路径很明确:先读 doc/WinFsp-Tutorial.asciidoc 跟着教程把 passthrough 完整写一遍,再看 tst/passthrough/(透传型示例,适合理解全量回调)和 tst/memfs/(内存型示例,适合做自定义逻辑的起点)。当你能在X:\里写出第一个文件时,恭喜——你已经是一个"用户模式文件系统开发者"了。

【免费下载链接】winfspWindows File System Proxy - FUSE for Windows项目地址: https://gitcode.com/gh_mirrors/wi/winfsp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

← 返回列表