最可靠方案:Windows用ReadDirectoryChangesW(需重叠I/O和手动解析)、Linux用inotify(注意队列溢出和递归)、macOS用FSEvents(需Run Loop调度);三者均须严格管理资源生命周期,跨平台不建议强行抽象。

Windows 上用 ReadDirectoryChangesW 监控文件夹最可靠
它不是轮询,是内核级异步通知,延迟低、资源省,Windows 原生方案里没比它更稳的。但必须配合重叠 I/O 和手动解析缓冲区,不能直接当回调函数用。
常见错误现象:ERROR_IO_PENDING 被当成失败处理;缓冲区太小导致事件丢失;没处理好“重命名后又新建同名文件”这类边界情况。
- 必须用
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED打开目录句柄 - 监控掩码选
FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE覆盖多数预警场景 - 缓冲区建议 ≥ 8KB(
sizeof(FILE_NOTIFY_INFORMATION) * 128),小于 4KB 极易截断 - 每次读完要手动遍历
FILE_NOTIFY_INFORMATION链表,NextEntryOffset为 0 才算结束
Linux 下优先用 inotify,别碰 dnotify 或轮询
inotify 是目前最轻量、最通用的方案,glibc 封装少,基本就是裸调 inotify_init1、inotify_add_watch 和 read。注意它不递归,子目录要单独加 watch。
容易踩的坑:IN_MOVED_TO 和 IN_MOVED_FROM 成对出现但可能跨两次 read;IN_Q_OVERFLOW 表示队列溢出,意味着你处理太慢或缓冲区太小;watch 数量受 /proc/sys/fs/inotify/max_user_watches 限制。
立即学习“C++免费学习笔记(深入)”;
- 创建 inotify 实例时传
IN_CLOEXEC,避免 fork 后泄漏 fd - 每个
read返回的是多个struct inotify_event拼接的二进制流,len字段才决定下一个事件偏移 - 对同一路径反复
add_watch不会报错,但会返回新 wd,旧的仍有效——得自己维护映射表防泄漏 - 想监控深层嵌套?得递归遍历目录树,对每个子目录调一次
inotify_add_watch
macOS 必须用 FSEvents,kqueue 只能凑合看单个文件
FSEvents 是 Apple 官方推荐且唯一支持目录树递归监控的 API,基于事件批处理,吞吐高但有轻微延迟(通常 ≤ 1 秒)。它不提供具体变更类型(比如改了哪行),只告诉你“某路径变了”,得自己对比 stat 或内容。
典型问题:FSEventStreamCreate 返回 NULL 却没报错信息;回调中调 dispatch_get_current_queue() 拿到的不是你预期的队列;忘记调 FSEventStreamScheduleWithRunLoop 导致事件永远不触发。
- 路径数组必须用
CFArrayRef,C 字符串要转CFStringRef再塞进去,不能直接传char** - 回调函数签名固定为
void (*)(ConstFSEventStreamRef, void *, size_t, char * const *, const FSEventStreamEventFlags *, const FSEventStreamEventId *),漏一个参数就 crash - 启用
kFSEventStreamCreateFlagFileEvents才能拿到文件级事件(否则只有目录变动) - 不要在回调里做耗时操作,尤其是同步磁盘 IO,会拖慢整个事件流
C++ 封装时最容易忽略的资源生命周期问题
所有平台的监控句柄/流对象都绑定系统资源:Windows 的 HANDLE、Linux 的 inotify fd、macOS 的 FSEventStreamRef。它们不是 RAII 友好的,析构时没显式清理就会泄漏或卡死后续监控。
更隐蔽的问题是:线程模型和事件分发耦合太紧。比如 Windows 的重叠 I/O 依赖 IOCP 或 GetQueuedCompletionStatus,Linux 的 inotify 通常要配合 epoll 或 poll,macOS 的 FSEventStream 必须跑在指定 RunLoop 上——这些没法靠一个统一接口抹平。
- Windows:用
CloseHandle关闭目录句柄前,先调CancelIoEx中止所有未完成的ReadDirectoryChangesW - Linux:关闭 inotify fd 前,先对每个 wd 调
inotify_rm_watch,否则内核里 watch 还挂着 - macOS:停用流必须按顺序调
FSEventStreamStop→FSEventStreamInvalidate→FSEventStreamRelease,少一步都可能 crash - 跨平台封装?别硬抽抽象层。宁可写三个独立模块,用预处理器或构建系统选其一,也别为了“统一接口”在回调里塞平台判断逻辑










