C#在Linux上用FileSystemWatcher性能差因默认轮询;需P/Invoke inotify系统调用,注意fd泄漏、路径权限、事件解析对齐及buffer溢出处理。

为什么 C# 在 Linux 上不能直接用 FileSystemWatcher
因为 FileSystemWatcher 在 .NET 6+ 虽已支持 Linux,但底层仍依赖 inotify,且默认启用的是“兼容模式”——它会轮询 /proc/self/fd 或 fallback 到 epoll + stat,导致高延迟、漏事件、CPU 升高。真要高效,得绕过它,直连 inotify 系统调用。
如何用 NativeLibrary 调用 inotify_init1 和 inotify_add_watch
需要手动 P/Invoke,关键点不是“能不能调”,而是“怎么避免 fd 泄漏和事件乱序”。
-
inotify_init1必须传IN_CLOEXEC | IN_NONBLOCK,否则子进程继承 fd 或阻塞读会导致死锁 - 监听路径要用绝对路径,相对路径在 chdir 后失效;且需确保对父目录有
EXECUTE权限(否则inotify_add_watch返回 -1,errno = EACCES) - 每次
read()必须循环解析inotify_event结构体,不能只读一次——因为内核可能 batch 多个事件进一个 buffer - 示例片段:
int fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); int wd = inotify_add_watch(fd, "/tmp", IN_CREATE | IN_DELETE | IN_MOVED_TO);
Span<byte> 解析 inotify_event 时的字节对齐陷阱
Linux 内核返回的 inotify_event 是变长结构:固定头(16 字节)+ 可选 name 字段(0 或 N 字节,以
Linux 内核返回的 inotify_event 是变长结构:固定头(16 字节)+ 可选 name 字段(0 或 N 字节,以 \0 结尾)。C# 的 Span<byte> 直接按结构体大小切片会越界或截断 name。
Span<byte> 直接按结构体大小切片会越界或截断 name。
- 必须先读取前 16 字节,提取
len字段(偏移 12,4 字节小端),再分配足够 buffer - name 字段长度不等于
len——len是整个 event 长度,name 实际长度是len - 16,且末尾 \0 不计入len - 用
Encoding.UTF8.GetString(span.Slice(16, nameLen)),别用Marshal.PtrToStringAnsi(遇到 \0 就停)
事件重复、丢失与重放边界在哪
inotify 本身不保证事件顺序或幂等性。比如 mv a b && mv b c 可能触发 MOVED_FROM+MOVED_TO,也可能合并为单个 MOVED_SELF(取决于是否跨文件系统)。更麻烦的是 buffer 溢出:
- 内核 inotify queue 默认仅 16384 字节,高频写入(如解压、日志刷盘)极易触发
IN_Q_OVERFLOW - 一旦溢出,后续所有事件丢弃,且不会补发——必须监听该事件并重建 watch
- 安全做法:每个
wd单独开线程 read + 解析,避免一个卡住阻塞全部;buffer size 至少设为 64KB,并检查read()返回值是否等于 buffer length(等于说明可能被截断)
实际用起来最易忽略的,是 inotify 实例生命周期必须与进程强绑定——fork 后子进程不会自动继承 inotify fd,且无法通过 dup 传递 watch 描述符(wd 是 per-inotify 实例的)。换言之,热更新或守护进程 reload 时,不重建 inotify 实例就会彻底失联。









