的虚拟文件系统架构与实践)
开发工具CLI后端【免费下载链接】saplingA Scalable, User-Friendly Source Control System.项目地址https://gitcode.com/gh_mirrors/sa/sapling点击查看免费下载本篇文章以 Sapling 仓库中 Windows.md 文档为主体系统讲解 EdenFS 在 Windows 平台上如何基于微软 Projected File SystemProjectedFS简称 PrjFS/ProjFS构建虚拟文件系统从选型历史、缓存态Cached State数据模型到读写/目录枚举回调、写通知无序处理、checkout 失效invalidation机制与 Windows 专属 FSCK并结合仓库源码PrjfsChannel.cpp、EdenConfig.h 等给出可验证的实现细节与配置项。读完你将理解 EdenFS 在 Windows 上与 FUSE/NFS 的本质差异掌握 PrjFS 占位符placeholder、水合hydrated、工作集working set等核心概念以及排查幽灵文件出现在sl status一类问题的完整推理链条。背景Windows 上的 EdenFS 为什么需要单独的文档EdenFS 在不同平台采用完全不同的文件系统接口在 Linux 上基于 FUSEFilesystem in Userspace在 macOS 上同时支持 FUSE 与 NFS而在 Windows 上则基于微软的ProjectedFS。ProjectedFS 的工作方式与前两者差异极大因此仓库中为它单独维护了这一篇文档。先澄清一个术语ProjectedFS 在微软文档与社区中被同时称为ProjFS与PrjFS两者指同一个东西。后续正文统一使用 PrjFS。另外文档默认读者已具备 FUSE 与 NFS 的相关知识可参考 macOS 文档 中对 NFS 局限性的讨论。历史为什么 EdenFS 最终选择了 PrjFS 而非自研驱动EdenFS 于 2015 年启动而Windows 支持始于 2018 年。最初的方案是编写自己的 Windows 内核驱动。但随后团队了解到微软当时正在开发 PrjFS——一家大厂正在做和自己想做的事情完全相同的事新的内核驱动于是 EdenFS 选择了与微软联手直接使用 PrjFS从而免去自研并长期维护内核驱动的巨大成本与风险。这一选择在后续的备选方案评估中也得到了反复印证。2023 年前后团队对以下备选方案进行了系统调查WinFSPWinFSP 同时提供 Windows 风格与 FUSE 风格的接口。Windows 风格接口性能可能更好但 FUSE 风格接口更容易与 EdenFS 集成。文档结论这是当时最有前景的备选方案。WinFUSE一个面向 Windows 的类 FUSE 内核驱动出现时间早于 WinFSP但 WinFSP 的支持更完善因此 WinFSP 更可能是更好的选择。不过文档也承认当时对 WinFUSE 的调查还不够深入。NFSEdenFS 在 macOS 上已有 NFS 支持如果 Windows 也走 NFS可以降低双平台的支持负担且 Windows 自带微软 NFS 客户端。但在 2023 年调查时最大的未解决问题是性能该 NFS 客户端在文件句柄全部关闭后似乎不再缓存文件数据导致性能远低于 PrjFS。此外Windows 走 NFS 还需要补齐大量能力——最大两块是overlay 中的文件内容存储以及让单个服务器服务所有 NFS 挂载。同时 NFS 也会带来 macOS 上同样的一堆限制奇怪的失效invalidation语义、拿不到客户端进程 PID 等。SMBSMB 与 NFS 类似但为 Windows 设计微软客户端可能有更好的 OS 级缓存支持。不过 WinFSP 与 EdenFS 这类文件系统的形态更接近集成更容易。若进一步考虑 SMB需要重点调查的仍是失效机制与性能。WSL2Windows Subsystem for Linux可以借助自动化让用户配置好 WSL2 后使用 EdenFS证书与网络操作也可解决。但问题在于重定向层级太多性能存疑微软官方不推荐跨文件系统工作而 WSL2 的跨文件系统性能比 WSL 更差参见微软的版本对比文档同时它难以支持依赖 Windows 特性的工作流如 Unity 等 Windows-only 工具。自研内核驱动Windows 允许第三方内核驱动但这需要巨大的开发量并要承担长期维护内核代码的额外风险。文档明确给出结论使用 PrjFS 是不需要自研驱动的关键原因。最终EdenFS 在 Windows 上坚定地选择了 PrjFS并围绕它的特性构建了一整套适配层。PrjFS 的工作原理Cached State缓存态模型PrjFS 的核心理念是让常见路径读取一个已被读过或修改过的文件零开销。为此文件状态完全由 PrjFS 管理并直接存储在 working copy工作副本中。EdenFS 只在 PrjFS 不知道文件状态时才参与提供数据——也就是说EdenFS 是按需提供者。一个重要的细节PrjFS 用路径而非 inode 号引用文件。当请求列目录或读文件时PrjFS 总是按路径向 EdenFS 提问。读取Reads占位符与水合首次打开一个文件时PrjFS 向 EdenFS 发送PRJ_GET_PLACEHOLDER_INFO_CB回调EdenFS 调用PrjWritePlaceholderInfoAPI 在 NTFS 后端文件系统中填充一个占位符placeholder。首次读取时PrjFS 发送PRJ_GET_FILE_DATA_CB回调EdenFS 调用PrjWriteFileData把文件内容写入 working copy此时文件成为hydrated placeholder水合占位符。此后对该文件的再次打开或读取不再经过 EdenFS直接由文件系统提供服务。在源码中这一过程对应 PrjfsChannel.cpp 的getPlaceholderInfo实现EdenFS 通过 dispatcher 的lookup查询 Sapling 树得到文件大小、目录标志、时间戳等元数据组装PRJ_PLACEHOLDER_INFO后调用PrjWritePlaceholderInfo若启用符号链接支持且目标为符号链接则调用带扩展信息的PrjWritePlaceholderInfo2。文件内容则通过getFileData中的PrjWriteFileData写入且按512 KiB ~ 5 MiB的块大小分块传输见 PrjfsChannel.cpp 的kMinChunkSize/kMaxChunkSize常量。文件状态机hydrated placeholder 是 PrjFS 术语。微软文档将其定义为一种缓存状态cache state。下图展示了文件在各种状态间的流转黑色为 PrjFS 状态红色为 EdenFS inode 状态黑色字标出用户触发的 Windows 文件系统调用蓝色标出 PrjFS 回调 EdenFS 的调用写入Writes先落盘后通知文件被写入时内容先写入磁盘然后 EdenFS 才通过PRJ_NOTIFICATION_CB具体为fileHandleClosedModified这类通知得知文件已变化。也就是说EdenFS 收到写通知时写入已经完成、已经对文件系统可见——这直接决定了后面EdenFS 无法阻止写入这一事实。目录Directories每次枚举都重新请求目录的行为比较特殊PrjFS 不记忆目录内容每次列出目录都会重新向 EdenFS 请求。目录枚举由三个回调组成阶段回调作用打开目录PRJ_START_DIRECTORY_ENUMERATION_CB开始一次目录枚举会话读取目录PRJ_GET_DIRECTORY_ENUMERATION_CB逐段返回目录项关闭目录PRJ_END_DIRECTORY_ENUMERATION_CB结束枚举会话需要特别注意的是EdenFS 只负责列出目录的原始状态减去本地目录修改。用户新建的、当前 Sapling 提交中不存在的目录不会收到这些回调PrjFS 自己负责保存目录的本地修改。如果 EdenFS 把本地修改混入目录枚举结果就会混淆 PrjFS。下图为目录枚举时 PrjFS 与 EdenFS 的交互Cached State 的五项深刻影响缓存态模型给 EdenFS 带来了五个与 Unix 平台截然不同的行为约束文档逐一做了阐述。1. 离线读写EdenFS 停止后文件仍可访问已经被读过的文件即使 EdenFS 停止后依然可读某些文件甚至在 EdenFS 未运行时也可以被写入。为了兜住这一点EdenFS 必须在每次启动时追平catch up离线期间发生的一切写入。在 Unix 上如果 EdenFS 未优雅关闭overlay本地修改文件的存储可能损坏由 FSCKFileSystem ChecK扫描修复且 FSCK 只在异常关闭后的下次启动时运行。Windows 上的 FSCK 被扩展为同时处理离线期间文件被修改与overlay 损坏两类问题并且每次启动都会运行——因为文件随时可能在 EdenFS 离线期间被修改而不只是异常关闭时。这也意味着Windows 的启动比其他平台更慢。FSCK 在 Windows 上的内部工作机制有专门文档Windows FSCK 详解。实现层面windowsFsckScanLocalChangesWindowsFsck.cpp会遍历 overlay 根目录并递归处理每个子目录将 EdenFS 的 inode 状态与磁盘/源控制树比对修正它默认使用全局 CPU 线程池执行fsck:multi-threaded配置为 false 时退化为串行执行并受prjfs:fsck-detect-renames配置控制是否检测重命名文件。2. EdenFS 只提供已提交数据Committed DataPrjFS 是本地文件包括已修改文件的唯一维护者。由此推出一个重要约束EdenFS 只能从当前 Sapling 提交commit提供文件数据与元数据。用户创建的文件不应出现在目录枚举结果中。更反直觉的是被重命名的文件PrjFS 总是用其重命名前的路径来引用参见微软 ProjFS-Managed-API 的 issue #68。因此 EdenFS 服务 PrjFS 回调时完全依赖 Sapling 树trees从不 consult inode 状态详见 Inodes 文档。这对下文要讲的失效机制有着微妙的影响。3. 工作集大小Working SetWindows 需要后台 GCEdenFS 会跟踪操作系统知道的文件集合即working set工作集。这样在执行sl checkout时EdenFS 才能告诉操作系统它知道的文件里哪些发生了变化。FUSE 通常只把文件缓存在内存中一小段时间并在不再记住文件内容时通知 EdenFS因此文件可以从 working set 中剔除后续 checkout 的工作量变小。但PrjFS 从不忘记文件working set 会无限膨胀checkout 因此变得很慢。对策是EdenFS 在 Windows 上维护一个后台 GC专门告诉 PrjFS 忘记某些文件——具体只针对未被本地修改且近期未被访问的文件。这一策略有效收紧了 working set使Windows 的 checkout 性能接近 Linux、并快于当前的 macOS。4. 本地修改文件的存储overlay 不再存内容FUSE 对文件内容的缓存本质上只是缓存EdenFS 才是真正的数据源但PrjFS 会持久保存一份文件内容副本并把这份副本当作事实来源source of truth。这扩大了 EdenFS 与文件系统之间脱节out of sync的 bug 类别。为减少重复状态EdenFS 不再把本地修改文件的内容存进 overlay而是直接从 repo 中读取。EdenFS 仍为整个 working set 维护内存中的 inode但这些 inode 只是磁盘上 repo 内容的缓存仅保存在内存中——因为磁盘上的事实来源已由 PrjFS 保管。FSCK 会在每次启动时重建完整的 inode 状态。5. 延迟LagEdenFS 的视图永远滞后靠 SyncBehavior 缓解由于写通知在修改完成后才送达EdenFS 的 inode 状态与磁盘状态之间存在时间差其文件系统视图总是滞后。虽然 Windows 上 EdenFS 很少使用 inode 状态文件读取直接由提交数据服务但 inode 仍是 EdenFS 内部运作的基础所有 Thrift 方法都依赖 inode。getScmStatus、checkoutRevision、globFiles等关心 working copy 状态的接口其数据恰恰是 PrjFS 不提供的因此必须查询 inode 状态。既然只有服务 Thrift 请求时才需要查询 inode 状态EdenFS 只需要保证在处理 Thrift 请求之前inode 状态已追平此前所有 working copy 变更。实现方式是EdenFS 内部维护一个需要追平的通知队列Thrift 操作可携带SyncBehavior参数触发 EdenFS 在处理调用前先处理完已收到的全部通知——做法是向这个串行处理的队列中追加一个空通知并等待其被处理这能让 Thrift 响应与文件系统更一致但仍无法覆盖EdenFS 尚未感知的并发写入。SyncBehavior允许客户端控制等待同步的时长。源码中getSyncTimeoutEdenServiceHandler.cpp实现了该语义syncTimeoutSeconds未设置时默认 60 秒为 0时立即返回不等待为负数时无限等待为正数时等待至超时。像 Buck、Watchman 这类客户端不介意数据略微过期可以传 0 或不等待。文档也指出理论上可以加入时间延迟/沉降/cookie类似 Watchman机制来兜住 EdenFS 尚未感知的修改但这会给所有Thrift 操作增加额外开销绝大多数场景下用户察觉不到滞后为罕见边界情况付出整体性能代价并不划算。写通知Write Notifications必须处理无序事件每当 working copy 发生写操作写文件、重命名、建目录等PRJ_NOTIFICATION_CB回调被触发。这个回调通常在写操作完成后才被调用因此EdenFS 无法拒绝该操作。该回调最微妙之处在于ProjectedFS 不保证通知的先后顺序。例如并发创建目录层级时子目录的通知可能先于其父目录到达文件与目录的删除同理。EdenFS 曾经用朴素的方式处理文件修改通知——直接把修改套用到 inode 状态上文件被删了就从 inode 层级里 unlink 它。在无序通知下这会导致四类错误先听到文件创建、后听到父目录创建尝试把新文件加入父目录 inode 时目录 inode 还不存在。若直接忽略该通知文件永远不会进入 inode导致它从sl status中消失。→ 必须自动创建缺失的父目录。先听到目录删除、后听到其中文件删除处理目录删除时目录可能非空必须递归删除随后处理文件删除时父目录 inode 已不存在必须优雅处理意识到工作已完成什么都不做。文件已存在先听到创建、后听到删除创建通知到来时发现文件已存在无事可做随后删除通知把文件删掉。最终 EdenFS 认为文件不存在而文件实际在磁盘上——sl status会把它报为已删除。文件不存在先听到删除、后听到创建删除通知到来时无事可做创建通知随后把文件建出来。最终 EdenFS 认为文件存在而磁盘上并没有——sl status会把一个磁盘上不存在的文件报为已修改。那么 EdenFS 如何正确应对所有通知在单个后台线程上串行处理。源码中PrjfsDispatcher.cpp 用单线程executor_{1, PrjfsDispatcher}外加folly::SerialExecutor包装出notificationExecutor_保证通知被顺序消费。处理过程非阻塞收到通知后先检查相关文件/目录的实际磁盘状态再据此更新 inode文件缺失就从 inode 层级移除、目录缺失就移除整棵目录层级等。核心原则不信任通知内容把通知当作这个文件周围发生了变化的信号只让 inode 状态去匹配磁盘上该文件/目录周围的真实状态。另外PrjfsChannel.cpp 还会拦截递归回调如果通知的触发进程是 EdenFS 自己TriggeringProcessId GetCurrentProcessId()直接返回ERROR_ACCESS_DENIED以避免 EdenFS 自触发通知造成死锁。Invalidationscheckout 时的文件失效机制如果在 checkout 过程中某个之前被读过的文件发生了变化EdenFS 必须告诉 PrjFS该文件变了否则 PrjFS 会继续向用户提供旧内容。这种告诉操作系统文件已变的动作称为invalidation失效通过PrjDeleteFileAPI 完成。目录则不同如前所述PrjFS 对大多数目录读取都会咨询 EdenFS但只对存在于当前提交中的目录如此用户新建的目录永远不会。因此当 checkout 造成目录内文件增删时EdenFS 仍需要失效目录若某个本地新建的目录在 checkout 期间发生变化或存在于目标提交中EdenFS 需要通过PrjMarkDirectoryAsPlaceholderAPI 为它补上占位符。值得说明的是微软文档并未记载该 API 可用于失效但 VFSForGit 正是用与 EdenFS 相同的方式做失效的。失效机制的已知问题失效已成为 EdenFS 多个 bug 的来源GUID 不匹配向PrjMarkDirectoryAsPlaceholder传入与根目录 GUID 不一致的 GUID有时会触发 Windows 报错The provider that supports file system virtualization is temporarily unavailable。为规避EdenFS 在创建挂载时把所用 GUID存入挂载配置并在整个工作副本生命周期内复用同一个 GUID源码见 PrjfsChannel.cpp挂载时用mountId_调用该 API。非填充目录上的递归回调对未填充的目录调用PrjMarkDirectoryAsPlaceholder会引发递归回调曾因反复尝试获取已持有的锁而导致 EdenFS 死锁。空目录限制PrjDeleteFile与PrjUpdateFileIfNeeded只能作用于空目录否则会报目录非空。前者报错可以理解后者则很意外。占位符版本信息优化受阻回调时 PrjFS 会传入文件相对路径以及占位符中存储的PRJ_PLACEHOLDER_VERSION_INFO可通过PrjWritePlaceholderInfo写入。本可以做优化——把 tree/file ID 存进占位符用其直接定位数据、免去遍历 Sapling 树。但由于PrjUpdateFileIfNeeded无法更新含未跟踪文件的目录的占位符checkout 后占位符会过期失效该优化无法落地。失效为何会让 EdenFS 与磁盘脱节五个事实的推演文档指出当下失效仍存在与磁盘 repo 状态脱节的问题并给出了严密的因果链。有五个事实叠加导致只要 PrjFS 对某文件持有占位符无论是否水合EdenFS 就会失效它EdenFS先失效文件、后更新 inode 状态若失效失败则 inode 不更新若其他进程持有该文件句柄失效会失败与 Windows 上常规删除因句柄占用失败同理但checkout 在失效失败的情况下依然会成功文件读取永远从已提交数据服务而非 inode。场景推演幽灵文件出现在sl status假设某文件是未水合占位符且被另一进程现实中通常是 Unity打开着句柄checkout 后该文件内容应改变。由 1EdenFS 会尝试失效它由 3句柄占用使失效失败由 2inode 不更新到下一提交由 4checkout 仍成功、当前检出提交被更新由 5该文件从未读过未水合下次读取会从新提交读取。于是inode 里是旧提交内容磁盘上是新提交内容——磁盘内容正确但文件会凭空出现在sl status中。更严重的场景内容错误若一个文件被重命名且仍是未水合占位符checkout 又删除/修改了原文件。由于 PrjFS 按名字问文件、EdenFS 按提交供数据checkout 后读取该文件时EdenFS 会返回新提交中原路径的文件内容即修改后的内容或报文件不存在错误。这与 Unix 行为相悖很可能是用户不期望的。修复方向要修复就必须打破上述 5 个事实之一文档逐一评估不失效未水合占位符、只更新其 inode但仍可能有文件大小等磁盘缓存数据普通占位符还是需要失效也许父目录失效就足够但尚不明确。无条件更新 inode与方案 1 类似但对水合/物化文件会导致 inode 与文件系统状态不一致因此或许只能对未水合占位符采用。无法改变——句柄占用是 Windows 的行为改不了。失效失败则 checkout 失败但今天不这样做正是因为在句柄频繁打开时 checkout 会经常失败。更好的用户体验或许是在sl中建立已成功但部分文件未更新的状态并给用户提供解决途径。读取时携带版本信息首次列出文件时提供版本commit信息读取时从那个提交读而非当前检出提交。这对目录会很棘手向占位符加入版本信息又会使PrjUpdateFileIfNeeded失败可能只能给文件而非树存版本信息仍会残留同步问题且会让磁盘上的内容更频繁地不更新——大概率要配合方案 4 使用。端到端请求流下图概括了 Windows 上一次完整请求占位符创建、读取、枚举、通知在 PrjFS 与 EdenFS 各组件之间的代码流陷阱与注意事项Pitfalls and CaveatsInvalidation如前文所述失效非常棘手会导致status出现幽灵文件、EdenFS 与磁盘状态脱节。重命名目录由于 ProjectedFS 跟踪 working copy 状态的方式它不支持重命名目录占位符。这已引发不少用户抱怨目前最好的补救是教育用户使用hg mvSapling 中对应sl mv而不是普通的mv。EdenFS 无法阻止写入写通知在写入完成后才发出EdenFS 无法拒绝必须照单全收——包括对.eden/config这一魔法配置的写入也无法阻止。离线写入EdenFS 未运行时文件仍可被写入因此每次启动都要处理对用户而言仓库看起来半可用能写但 Sapling 操作/Buck 操作会失败容易造成困惑。延迟LagEdenFS 的视图始终滞后只能靠SyncBehavior缓解无法根除。符号链接Symlinks旧版 PrjFS 不支持符号链接直到 2024 年 PrjFS 才修补了符号链接处理中的几个大 bug。源码中符号链接支持通过enableSymlinks_开关控制支持时使用PRJ_EXT_INFO_TYPE_SYMLINK扩展信息写入占位符PrjfsChannel.cpp。关键 PrjFS API 速查微软 PrjFS 通过一组 API 与回调与 EdenFS 交互以下是文档中涉及的完整清单及其在仓库源码中的对应调用点API/回调用途源码调用点PRJ_GET_PLACEHOLDER_INFO_CB首次打开文件时询问占位符元数据PrjfsChannel.cppPrjWritePlaceholderInfo/PrjWritePlaceholderInfo2在 NTFS 中写入占位符可含符号链接扩展信息PrjfsChannel.cppPRJ_GET_FILE_DATA_CB首次读取时请求文件内容PrjfsChannel.cppPrjWriteFileData把文件内容写入 working copy水合PrjfsChannel.cppPRJ_START/GET/END_DIRECTORY_ENUMERATION_CB目录枚举三阶段回调PrjfsChannel.cppPRJ_NOTIFICATION_CB写操作完成后的通知无序、不可拒绝PrjfsChannel.cppPrjDeleteFilecheckout 时失效文件仅限空目录PrjfsChannel.cppPrjMarkDirectoryAsPlaceholder挂载建占位、checkout 时补目录占位符/失效PrjfsChannel.cppPrjUpdateFileIfNeeded按需更新占位符含未跟踪文件的目录会失败文档讨论未落地用于版本信息优化PRJ_PLACEHOLDER_VERSION_INFO占位符中可携带的版本信息PrjfsChannel.cpp相关配置项参考EdenFS 的 [prjfs] 配置段定义于 EdenConfig.h直接控制 Windows 上的 PrjFS 行为配置项默认值说明prjfs:request-timeout1 分钟PrjFS 回调允许的最大时长超时失败并向用户告警避免永久阻塞prjfs:use-negative-path-cachingtrue启用 PrjFS 负路径缓存减少对不存在文件的请求数仅 Windows 生效prjfs:num-invalidation-threads1每个挂载用于目录失效的线程数prjfs:directory-creation-delay100 ms收到目录创建通知后的等待/重试延迟规避符号链接两阶段创建与目录通知的竞态prjfs:listen-to-pre-convert-to-fullfalse监听PRJ_NOTIFY_FILE_PRE_CONVERT_TO_FULL通知ProjFS 存在文件关闭修改通知丢失bug 时用于兜底截断状态prjfs:fsck-detect-renamestrue让 FSCK 检测并修正重命名文件否则 EdenFS 可能与文件系统脱节开启会使 FSCK 变慢prjfs:torn-read-log-interval10 s对torn read文件在读取中途被修改的日志/Scuba 上报频率fsck:multi-threadedtrue是否用多线程执行 FSCK定义于 EdenConfig.h延伸阅读Windows FSCK 详解FSCK 在 Windows 上的完整工作机制、历史 bug 与修复方案含重命名/删除场景的状态表macOS 文档NFS 在 macOS 上的局限性Inodes 文档EdenFS 的 inode 层级模型PrjfsChannel.cppWindows 挂载通道、回调与失效的核心实现PrjfsDispatcher.cpp通知串行执行器与请求分发WindowsFsck.cppWindows FSCK 的扫描实现赞分享开发工具CLI后端【免费下载链接】saplingA Scalable, User-Friendly Source Control System.项目地址https://gitcode.com/gh_mirrors/sa/sapling点击查看免费下载相关推荐EdenFS on macOS基于 NFSv3 的内核集成架构、限制与运维实践Sapling 项目深度解析EdenFS on macOS基于 NFSv3 的内核集成架构、限制与运维实践Sapling 项目深度解析 本文以 Sapling 仓库中 eden/fs开发工具CLI后端Sapling/EdenFS Macro 基准测试指南基于文件系统与 Thrift API 的性能评测体系Sapling/EdenFS Macro 基准测试指南基于文件系统与 Thrift API 的性能评测体系 eden/fs/benchmarks/ 目录承开发工具CLI后端Sapling SCM虚拟文件系统EdenFS提升大型仓库性能的秘密武器Sapling SCM虚拟文件系统EdenFS提升大型仓库性能的秘密武器 Sapling SCM的EdenFS虚拟文件系统是专为大型代码仓库设计的性能优化工具开发工具CLI后端上一篇从scm-1到语义化nvim-lspconfig版本管理痛点与解决方案下一篇从手动操作到智能托管AhabAssistantLimbusCompany如何彻底改变你的《Limbus Company》游戏体验创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考