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

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+,且未设置环境变量 MonoEnablePolling 或 DOTNET_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+libinotifyP/Invoke,或封装epoll监听inotifyfd(推荐库:Microsoft.Extensions.FileSystemGlobbing不适用,需用inotify-csharp等轻量绑定) - macOS:用
CoreFoundation.CFFileDescriptor监听kqueue事件,或采用fsevents(更高效但仅限 HFS+/APFS) - 跨平台折中:用
Microsoft.Extensions.Hosting.IHostedService启动后台轮询,但改用Directory.EnumerateFileSystemEntries+GetFileSystemEntryInfo做增量哈希比对,避免全量扫描
真正的“底层”不是换一个托管类,而是接受需要写 platform-specific interop 的事实 —— .NET 的抽象层在这里有意牺牲了精确性来换取一致性。










