应抽象统一事件结构和接口,Linux用inotify(注意递归、缓冲区、fd泄漏),Windows用ReadDirectoryChangesW(需异步IO、大缓冲区、手动递归),并妥善处理线程安全、路径编码及重命名事件合并。

用 inotify 和 ReadDirectoryChangesW 分别处理 Linux 与 Windows
跨平台文件监视没有统一 API,C++ 标准库至今不提供原生支持。必须按系统分路径实现:Linux 用 inotify,Windows 用 ReadDirectoryChangesW。macOS 虽有 FSEvents,但多数项目只保 Linux/Windows 双平台,暂不引入第三方(如 libuv)时,手动桥接最可控。
关键不是“怎么写两个版本”,而是“怎么让上层逻辑不感知差异”。建议抽象出统一的事件结构:
struct FileEvent {
enum Type { Created, Deleted, Modified, Renamed };
Type type;
std::string path;
std::string old_path; // 仅重命名时有效
};
然后为每个平台实现独立的 watcher 类(如 InotifyWatcher、Win32Watcher),都继承同一基类或满足相同接口契约。
Linux 下 inotify 的常见陷阱:漏事件、fd 泄漏、缓冲区溢出
inotify 不是“监听目录就自动递归”,默认只监控一级子项;也不保证事件不丢失——尤其高频率写入时,内核缓冲区满就会丢弃后续事件,且不报错。
- 务必调用
inotify_add_watch(fd, path.c_str(), IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVED_TO | IN_MOVED_FROM),显式指定所有需捕获的事件类型;IN_ALL_EVENTS在旧内核可能不可用 - 递归监听需自己遍历子目录并为每个目录调用
inotify_add_watch,注意深度限制和符号链接循环 - 读取
read(inotify_fd, buf, sizeof(buf))后,必须完整解析struct inotify_event链表,不能只读一次就停;否则残留未处理事件会堆积 - 每次
inotify_add_watch返回新 watch descriptor,但没对应 remove 接口,watch 项数受/proc/sys/fs/inotify/max_user_watches限制,长期运行服务需定期检查是否接近上限
Windows 下 ReadDirectoryChangesW 必须配合异步 I/O 和重叠结构
同步调用 ReadDirectoryChangesW 会阻塞线程,且单次调用只返回一批事件;而它本身不支持递归,也容易因缓冲区太小导致 ERROR_NOTIFY_ENUM_DIR 错误(即通知被截断)。
- 必须使用
OVERLAPPED结构 +IOCP或事件对象来实现非阻塞轮询;直接 while 循环调用同步版等于自废性能 - 缓冲区大小建议 ≥ 64KB(
std::vector),否则高频操作下极易触发截断,且截断后不会补发丢失事件buffer(65536) - 递归需手动开启
FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE等标志,并传入TRUE作为第 5 个参数(bWatchSubtree) - 返回的
FILE_NOTIFY_INFORMATION是变长结构,需用NextEntryOffset字段链式遍历,不能简单按固定长度拆分
封装时如何避免线程安全与资源生命周期问题
文件监视天然需要后台线程持续 read 或 GetQueuedCompletionStatus,但用户代码可能随时析构 watcher 对象,此时必须确保:内核句柄关闭、工作线程退出、未处理事件清空。
- Linux 版不要在析构函数里直接
close(inotify_fd)—— 若另一线程正read中,会导致EINTR或未定义行为;应先write(pipe_fd[1], "q", 1)通知线程退出,再join - Windows 版若用 IOCP,析构前必须调用
CancelIoEx中止所有挂起的ReadDirectoryChangesW请求,否则线程可能永远卡在GetQueuedCompletionStatus - 事件回调最好通过
std::function注册,但注意该函数对象的生命周期必须长于 watcher 实例;更稳妥做法是用std::shared_ptr管理回调上下文 - 路径编码:Linux 用 UTF-8,Windows 用 UTF-16;统一转成
std::u8string(C++20)或内部存std::string(UTF-8)+ Windows 上调用WideCharToMultiByte转换,避免std::filesystem::path在跨平台事件中隐式转换出错
最难缠的其实是 rename 场景:Linux 的 IN_MOVED_FROM + IN_MOVED_TO 是两条独立事件,Windows 的 FILE_ACTION_RENAMED_OLD_NAME + FILE_ACTION_RENAMED_NEW_NAME 也是分离的,合并逻辑必须靠时间戳或 cookie 匹配,稍有延迟就对不上。这个细节几乎没人提,但线上出过多次误报。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











