C#文件系统WatchService .NET在Linux/macOS上如何使用底层文件监控

3次阅读

linux/macos上FileSystemWatcher常不触发因默认轮询,.net 6+才支持inotify/kqueue内核通知,需确保环境变量未强制轮询、路径绝对、权限正确且InternalBufferSize非零。

C#文件系统WatchService .NET在Linux/macOS上如何使用底层文件监控

Linux/macOS 上 FileSystemWatcher 为什么经常不触发或漏事件

因为 .NET 的 FileSystemWatcher 在非 windows 平台默认回退到轮询(polling)模式,而非使用内核级通知机制。它会定期调用 stat() 检查文件时间戳/大小变化,延迟高(默认间隔 5 秒)、CPU 占用高、且无法捕获重命名、硬链接创建等元数据变更。

  • 可通过 FileSystemWatcher.EnableRaisingEvents = true 后检查 FileSystemWatcher.InternalBufferSize 是否为 0 来确认是否在轮询 —— 非零值才表示启用了内核通知(如 inotify/kqueue)
  • Linux 下需确保进程有权限访问 /proc/sys/fs/inotify/max_user_watches,否则初始化时静默失败或抛 IOException
  • macOS 上 .NET 6+ 才通过 kqueue 实现真正异步监控;.NET 5 及更早版本始终轮询

如何强制启用 inotify(Linux)或 kqueue(macos

必须满足两个前提:运行时是 .NET 6+,且未设置环境变量 MonoEnablePollingDOTNET_SYSTEM_IO_ENABLE_POLLING(设为 true 会强制轮询)。

  • 启动前清除干扰变量:unset DOTNET_SYSTEM_IO_ENABLE_POLLING
  • 检查是否生效:构造 FileSystemWatcher 后立即读取 watcher.InternalBufferSize —— Linux 上典型值为 8192,macOS 上为 1024,均为非零即成功
  • 路径必须为绝对路径;相对路径会导致底层初始化失败并静默降级
  • 监听目录需有可读 + 执行(rx)权限,否则 inotify 不会注册监听项

FileSystemWatcher 在 Linux/macOS 上的事件局限性

即使启用了 inotify/kqueue,.NET 仍做了跨平台抽象,导致部分底层事件被过滤或合并:

  • Renamed 事件在 inotify 中对应 IN_MOVED_TO/IN_MOVED_FROM,但若重命名跨文件系统(如从 /tmp/home),会拆成 Created + Deleted,而非单个 Renamed
  • Changed 事件默认只报告 LastWrite,不区分内容修改与属性变更(如 chmod);需手动设置 NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Attributes
  • inotify 不递归监听子目录 —— IncludeSubdirectories = true 是 .NET 层模拟的:对每个新目录单独调用 inotify_add_watch,存在竞态(新建目录后立即写入文件可能丢失事件)

需要可靠监控时该用什么替代方案

如果业务要求低延迟、不丢事件、支持硬链接/符号链接追踪或跨文件系统重命名识别,应绕过 FileSystemWatcher,直接对接原生 API:

  • Linux:用 System.IO.Pipelines + libinotify P/Invoke,或封装 epoll 监听 inotify fd(推荐库:microsoft.Extensions.FileSystemGlobbing 不适用,需用 inotify-csharp 等轻量绑定)
  • macOS:用 CoreFoundation.CFFileDescriptor 监听 kqueue 事件,或采用 fsevents(更高效但仅限 HFS+/APFS)
  • 跨平台折中:用 Microsoft.Extensions.Hosting.IHostedService 启动后台轮询,但改用 Directory.EnumerateFileSystemEntries + GetFileSystemEntryInfo 做增量哈希比对,避免全量扫描

真正的“底层”不是换一个托管类,而是接受需要写 platform-specific interop 的事实 —— .NET 的抽象层在这里有意牺牲了精确性来换取一致性。

text=ZqhQzanResources