47、文件监控:inotify(Linux)、ReadDirectoryChanges(Windows)
文件监控,说白了就是让程序能「感知」到文件的变化。
你想想看,一个后台服务跑着,突然配置文件被人改了,日志文件被轮转了,或者用户上传了一个新文件——如果程序不能实时知道这些事,那只能靠轮询,每隔几秒扫一遍目录。效率低不说,还容易漏掉瞬间的变化。
我早年做网盘同步工具时,就吃过轮询的亏。用户说「我明明改了文件,怎么半天没同步?」——嗯,后来换成 inotify,问题就解决了。
47.1 为什么需要文件监控?
文件监控的核心价值就三个字:实时性。
- 配置热加载:服务不用重启,配置文件一改,自动生效。
- 日志轮转检测:日志文件被切割时,程序能重新打开新文件。
- 文件同步/备份:监控目录变化,增量同步。
- 安全审计:谁改了哪个文件,第一时间记录。
说白了,就是让程序从「被动等待」变成「主动感知」。
核心区别:轮询是「你问我答」,监控是「你变我知」。后者延迟通常在毫秒级,前者至少秒级。
47.2 Linux 方案:inotify
inotify 是 Linux 内核从 2.6.13 开始提供的文件系统事件监控机制。我个人的习惯是,只要目标系统内核版本够,优先用 inotify。
47.2.1 基本用法
使用 inotify 只需要三步:
- 调用
inotify_init()创建一个监控实例,返回一个文件描述符。 - 调用
inotify_add_watch()添加要监控的文件或目录,指定事件类型。 - 用
read()从文件描述符中读取事件。
来看一个最简单的例子:
#include <sys/inotify.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int fd = inotify_init();
if (fd == -1) {
perror("inotify_init");
return 1;
}
int wd = inotify_add_watch(fd, "/tmp",
IN_CREATE | IN_DELETE | IN_MODIFY);
if (wd == -1) {
perror("inotify_add_watch");
return 1;
}
char buf[4096];
while (1) {
ssize_t len = read(fd, buf, sizeof(buf));
if (len == -1) break;
struct inotify_event *event = (struct inotify_event *)buf;
// 注意:buf 中可能包含多个事件,需要循环处理
printf("事件: mask=%u, name=%s\n",
event->mask, event->name);
}
close(fd);
return 0;
}
个人经验:read() 返回的 buf 里可能包含多个 inotify_event 结构体,每个结构体长度不同。一定要用 event->len 来跳转到下一个事件,别直接加 sizeof。
47.2.2 常见事件类型
| 宏定义 | 含义 | 我遇到过的问题 |
|---|---|---|
| IN_ACCESS | 文件被读取 | 监控日志文件时,这个事件会频繁触发 |
| IN_MODIFY | 文件被修改 | 最常用的事件,注意大文件会触发多次 |
| IN_CREATE | 文件/目录被创建 | 监控上传目录时必用 |
| IN_DELETE | 文件/目录被删除 | 小心:删除后 watch 会自动失效 |
| IN_MOVED_FROM / IN_MOVED_TO | 文件被移走/移入 | 原子重命名操作,比 CREATE+DELETE 更可靠 |
注意:inotify 默认不监控子目录。要递归监控,需要自己遍历子目录并为每个子目录添加 watch。我曾经因为这个踩过坑——监控了一个目录,结果子目录里的文件变化完全没反应。
47.2.3 性能与限制
inotify 有几个限制你需要知道:
- watch 数量上限:通过
/proc/sys/fs/inotify/max_user_watches控制,默认 8192。监控大量文件时记得调大。 - 事件队列大小:通过
/proc/sys/fs/inotify/max_user_instances控制。事件太多来不及读,会溢出丢事件。 - 不支持网络文件系统:NFS、CIFS 等远程文件系统上 inotify 可能不工作。
我个人建议:生产环境中,如果监控的文件数超过 10000,考虑用 fanotify 或者干脆分层监控。
47.3 Windows 方案:ReadDirectoryChangesW
Windows 上对应的 API 是 ReadDirectoryChangesW。说实话,这个 API 比 inotify 复杂不少,但功能更强大。
47.3.1 基本用法
核心思路是:打开一个目录句柄,然后调用 ReadDirectoryChangesW 异步等待变化。
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hDir = CreateFile(
L"C:\\Temp",
FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
NULL
);
if (hDir == INVALID_HANDLE_VALUE) {
printf("打开目录失败: %lu\n", GetLastError());
return 1;
}
char buf[4096];
DWORD bytesReturned;
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
while (1) {
BOOL ok = ReadDirectoryChangesW(
hDir,
buf,
sizeof(buf),
TRUE, // 递归监控子目录
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_LAST_WRITE,
&bytesReturned,
&overlapped,
NULL
);
if (!ok) {
printf("ReadDirectoryChangesW 失败\n");
break;
}
WaitForSingleObject(overlapped.hEvent, INFINITE);
FILE_NOTIFY_INFORMATION *info = (FILE_NOTIFY_INFORMATION *)buf;
// 处理事件...
printf("文件变化: Action=%d\n", info->Action);
}
CloseHandle(hDir);
return 0;
}
避坑指南:我曾经在 Windows Server 2008 上部署监控程序,发现 ReadDirectoryChangesW 偶尔会丢失事件。后来查文档才发现,如果缓冲区太小,API 会返回 ERROR_NOTIFY_ENUM_DIR 错误。解决方案是把缓冲区设大,至少 64KB。
47.3.2 事件类型对比
| Windows 事件 | 对应 Linux 事件 | 说明 |
|---|---|---|
| FILE_ACTION_ADDED | IN_CREATE | 文件/目录被创建 |
| FILE_ACTION_REMOVED | IN_DELETE | 文件/目录被删除 |
| FILE_ACTION_MODIFIED | IN_MODIFY | 文件内容被修改 |
| FILE_ACTION_RENAMED_OLD_NAME | IN_MOVED_FROM | 重命名前的旧名称 |
| FILE_ACTION_RENAMED_NEW_NAME | IN_MOVED_TO | 重命名后的新名称 |
47.4 跨平台方案对比
如果你需要写跨平台代码,我建议这样选:
- 纯 C 项目:用条件编译,Linux 用 inotify,Windows 用 ReadDirectoryChangesW。
- C++ 项目:可以考虑用 Boost.Asio 的 directory_monitor,或者 libuv 的文件监控接口。
- 嵌入式/老旧系统:老老实实轮询,别折腾。
我的建议:如果项目不大,直接用 libuv 或者 glib 的 GFileMonitor。它们封装了底层差异,你只需要写一套回调函数就行。我在一个跨平台配置管理工具里就这么干的,省了不少事。
47.5 核心知识体系
下面这张图总结了文件监控的核心逻辑:
这张图展示了文件监控的完整流程:应用程序发起监控请求,通过系统调用层进入内核(Linux)或系统 API(Windows),事件发生后通过队列或缓冲区传递给应用层,最后由回调函数处理。
47.6 实战建议
最后,分享几个我在实际项目中积累的经验:
- 缓冲区要够大:无论是 inotify 的 read buffer 还是 Windows 的 notify buffer,太小都会丢事件。我一般设 64KB 起步。
- 注意事件合并:同一个文件短时间内多次修改,inotify 可能会合并成一次事件。如果你需要记录每次修改,得自己加时间戳判断。
- 处理删除事件要小心:文件被删除后,对应的 watch 描述符就失效了。记得从监控列表中移除,否则下次 read 会报错。
- 递归监控要自己实现:两个平台都不自动监控子目录。我习惯用一个哈希表维护所有子目录的 watch 描述符,新增子目录时自动添加监控。
重要提醒:文件监控不是万能的。它只能告诉你「文件变了」,但变的是什么内容,还得你自己去读。另外,监控大量文件时,事件风暴可能导致程序 CPU 飙升——我见过一个监控 5 万个文件的程序,日志轮转时瞬间产生几千个事件,直接把程序卡死了。解决方案是加一个节流阀,比如 100ms 内同一个文件的事件只处理一次。
好了,文件监控这块就聊到这儿。记住一句话:能用系统 API 就别自己轮询,省心省力还高效。
公众号:蓝海资料掘金营,微信 deep3321