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,否则初始化时静默失败或抛
IOExceptionmacOS 上 .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.Attributesinotify 不递归监听子目录 ——
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 的抽象层在这里有意牺牲了精确性来换取一致性。
