文件系统监控:inotify(Linux)、ReadDirectoryChangesW(Windows)、文件变化通知
文件系统监控,说白了就是让程序能“听到”文件被改了、被删了、被挪走了。我刚开始做嵌入式开发时,总觉得这功能是桌面应用才需要的。直到有一次,我在一个数据采集系统里,需要实时响应配置文件的变化——嗯,从那以后,我再也不敢小看文件监控了。
这一章,咱们聊聊 Linux 和 Windows 上最主流的两种文件监控方案。你想想看,跨平台项目里,这块往往是让人头疼的地方。
为什么需要文件系统监控?
我遇到过不少场景,文件监控是刚需:
- 配置文件热加载:程序跑着跑着,配置文件改了,总不能重启吧?
- 日志文件轮转:日志文件被切割了,监控程序得知道该读哪个新文件。
- 数据同步:某个目录有新文件写入,立刻触发上传或备份。
- 安全审计:监控关键目录,谁动了文件,立刻报警。
说白了,文件监控就是给程序装上一双“眼睛”。
Linux 方案:inotify
Linux 上,inotify 是内核提供的文件系统事件监控机制。我个人习惯用 inotify 来处理配置文件热加载,因为它轻量、高效,而且不需要轮询。
inotify 的核心概念
- inotify_init():创建一个 inotify 实例,返回一个文件描述符。
- inotify_add_watch():添加监控项,指定要监控的文件或目录,以及感兴趣的事件类型。
- read():从 inotify 文件描述符读取事件。
- inotify_rm_watch():移除监控项。
常用事件类型
| 事件宏 | 含义 |
|---|---|
| IN_ACCESS | 文件被访问(读取) |
| IN_MODIFY | 文件被修改 |
| IN_ATTRIB | 文件属性被修改(如权限、时间戳) |
| IN_CLOSE_WRITE | 文件被关闭,且之前是写打开状态 |
| IN_MOVED_FROM | 文件被移出监控目录 |
| IN_MOVED_TO | 文件被移入监控目录 |
| IN_CREATE | 文件或目录被创建 |
| IN_DELETE | 文件或目录被删除 |
| IN_DELETE_SELF | 被监控的文件或目录本身被删除 |
一个简单的 inotify 示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#define EVENT_SIZE (sizeof(struct inotify_event))
#define BUF_LEN (1024 * (EVENT_SIZE + 16))
int main() {
int fd = inotify_init();
if (fd < 0) {
perror("inotify_init");
exit(EXIT_FAILURE);
}
int wd = inotify_add_watch(fd, "/tmp/watch_dir",
IN_CREATE | IN_DELETE | IN_MODIFY);
if (wd < 0) {
perror("inotify_add_watch");
close(fd);
exit(EXIT_FAILURE);
}
char buffer[BUF_LEN];
while (1) {
int len = read(fd, buffer, BUF_LEN);
if (len < 0) {
perror("read");
break;
}
int i = 0;
while (i < len) {
struct inotify_event *event = (struct inotify_event *)&buffer[i];
if (event->len) {
if (event->mask & IN_CREATE)
printf("文件创建: %s\n", event->name);
else if (event->mask & IN_DELETE)
printf("文件删除: %s\n", event->name);
else if (event->mask & IN_MODIFY)
printf("文件修改: %s\n", event->name);
}
i += EVENT_SIZE + event->len;
}
}
close(wd);
close(fd);
return 0;
}
fcntl(fd, F_SETFL, O_NONBLOCK) 设置为非阻塞模式。我在一个日志监控工具里就这么干过,配合 epoll 一起用,效果很好。
Windows 方案:ReadDirectoryChangesW
Windows 上,对应的 API 是 ReadDirectoryChangesW。说实话,这个 API 比 inotify 要复杂一些,参数多,而且有坑。我曾经在一个 Windows 服务程序里用它监控共享目录,踩了不少坑。
核心 API
- CreateFile():打开要监控的目录,获取句柄。
- ReadDirectoryChangesW():读取目录变化通知。
- FILE_NOTIFY_INFORMATION:事件数据结构。
关键参数
| 参数 | 说明 |
|---|---|
| hDirectory | 目录句柄,需用 FILE_LIST_DIRECTORY 权限打开 |
| lpBuffer | 接收事件数据的缓冲区 |
| nBufferLength | 缓冲区大小,建议 64KB 以上 |
| bWatchSubtree | 是否监控子目录 |
| dwNotifyFilter | 要监控的事件类型,如 FILE_NOTIFY_CHANGE_FILE_NAME |
| lpOverlapped | 用于异步操作的 OVERLAPPED 结构 |
一个简单的 Windows 示例
#include <windows.h>
#include <stdio.h>
#define BUFFER_SIZE 65536
int main() {
HANDLE hDir = CreateFile(
L"C:\\WatchDir",
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("打开目录失败: %d\n", GetLastError());
return 1;
}
char buffer[BUFFER_SIZE];
DWORD bytesReturned;
while (1) {
if (ReadDirectoryChangesW(
hDir,
buffer,
BUFFER_SIZE,
TRUE, // 监控子目录
FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME |
FILE_NOTIFY_CHANGE_LAST_WRITE,
&bytesReturned,
NULL,
NULL
)) {
FILE_NOTIFY_INFORMATION *pNotify = (FILE_NOTIFY_INFORMATION *)buffer;
do {
wchar_t fileName[MAX_PATH];
wcsncpy(fileName, pNotify->FileName,
pNotify->FileNameLength / sizeof(wchar_t));
fileName[pNotify->FileNameLength / sizeof(wchar_t)] = L'\0';
switch (pNotify->Action) {
case FILE_ACTION_ADDED:
wprintf(L"文件创建: %s\n", fileName);
break;
case FILE_ACTION_REMOVED:
wprintf(L"文件删除: %s\n", fileName);
break;
case FILE_ACTION_MODIFIED:
wprintf(L"文件修改: %s\n", fileName);
break;
case FILE_ACTION_RENAMED_OLD_NAME:
wprintf(L"文件重命名(旧): %s\n", fileName);
break;
case FILE_ACTION_RENAMED_NEW_NAME:
wprintf(L"文件重命名(新): %s\n", fileName);
break;
}
if (pNotify->NextEntryOffset == 0)
break;
pNotify = (FILE_NOTIFY_INFORMATION *)
((BYTE *)pNotify + pNotify->NextEntryOffset);
} while (1);
} else {
printf("ReadDirectoryChangesW 失败: %d\n", GetLastError());
break;
}
}
CloseHandle(hDir);
return 0;
}
跨平台对比
| 特性 | inotify (Linux) | ReadDirectoryChangesW (Windows) |
|---|---|---|
| 事件粒度 | 细粒度,支持多种事件 | 较粗,部分事件需组合判断 |
| 子目录监控 | 需递归添加 watch | bWatchSubtree 参数直接支持 |
| 缓冲区管理 | 内核管理,用户只需 read | 用户自己管理,容易溢出 |
| 异步支持 | 配合 epoll/select | 配合 I/O 完成端口 |
| 最大监控数 | 受 /proc/sys/fs/inotify/max_user_watches 限制 | 受系统资源限制 |
文件变化通知的通用设计模式
不管用哪个平台,文件监控的架构其实差不多。我画了一张图,帮你理清思路:
这个架构的核心思想是:上层不关心底层是 inotify 还是 ReadDirectoryChangesW。我在一个跨平台文件同步工具里就是这么设计的,上层只注册回调,底层自动选择平台实现。这样换平台时,只需要改底层那一小块代码。
避坑指南
我曾经踩过的坑,分享给你:
- 事件丢失:inotify 的环形缓冲区满了会丢事件。解决办法是增大
/proc/sys/fs/inotify/max_user_watches和max_queued_events。 - 重复事件:一个文件修改可能触发多次 IN_MODIFY。我一般加一个去重逻辑,比如 100ms 内同一文件只处理一次。
- 目录监控的递归问题:inotify 不会自动监控子目录,需要自己递归添加 watch。Windows 的 bWatchSubtree 虽然方便,但性能开销大,监控大目录时要小心。
- 文件句柄泄漏:Windows 上 CreateFile 打开的目录句柄,用完一定要 CloseHandle。我见过线上服务因为句柄泄漏,最后把系统资源耗光了。
总结
文件系统监控,说白了就是让程序具备“感知”文件变化的能力。Linux 用 inotify,Windows 用 ReadDirectoryChangesW,两者各有千秋。我个人建议,跨平台项目最好封装一层抽象接口,把平台差异隔离掉。
嗯,这一章就聊到这儿。记住,文件监控不是银弹——它解决的是“实时感知”的问题,但事件处理的可靠性、性能、资源管理,都需要你仔细设计。下次写文件监控代码时,想想我提到的那些坑,能省不少调试时间。