47、文件监控:inotify(Linux)、ReadDirectoryChanges(Windows)

文件监控,说白了就是让程序能「感知」到文件的变化。

你想想看,一个后台服务跑着,突然配置文件被人改了,日志文件被轮转了,或者用户上传了一个新文件——如果程序不能实时知道这些事,那只能靠轮询,每隔几秒扫一遍目录。效率低不说,还容易漏掉瞬间的变化。

我早年做网盘同步工具时,就吃过轮询的亏。用户说「我明明改了文件,怎么半天没同步?」——嗯,后来换成 inotify,问题就解决了。

47.1 为什么需要文件监控?

文件监控的核心价值就三个字:实时性

  • 配置热加载:服务不用重启,配置文件一改,自动生效。
  • 日志轮转检测:日志文件被切割时,程序能重新打开新文件。
  • 文件同步/备份:监控目录变化,增量同步。
  • 安全审计:谁改了哪个文件,第一时间记录。

说白了,就是让程序从「被动等待」变成「主动感知」。

核心区别:轮询是「你问我答」,监控是「你变我知」。后者延迟通常在毫秒级,前者至少秒级。

47.2 Linux 方案:inotify

inotify 是 Linux 内核从 2.6.13 开始提供的文件系统事件监控机制。我个人的习惯是,只要目标系统内核版本够,优先用 inotify。

47.2.1 基本用法

使用 inotify 只需要三步:

  1. 调用 inotify_init() 创建一个监控实例,返回一个文件描述符。
  2. 调用 inotify_add_watch() 添加要监控的文件或目录,指定事件类型。
  3. 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: inotify inotify_init / add_watch Windows: ReadDirectoryChangesW CreateFile + 异步 I/O 事件队列 (read) 事件缓冲区 (OVERLAPPED) 回调处理 / 业务逻辑

这张图展示了文件监控的完整流程:应用程序发起监控请求,通过系统调用层进入内核(Linux)或系统 API(Windows),事件发生后通过队列或缓冲区传递给应用层,最后由回调函数处理。

47.6 实战建议

最后,分享几个我在实际项目中积累的经验:

  • 缓冲区要够大:无论是 inotify 的 read buffer 还是 Windows 的 notify buffer,太小都会丢事件。我一般设 64KB 起步。
  • 注意事件合并:同一个文件短时间内多次修改,inotify 可能会合并成一次事件。如果你需要记录每次修改,得自己加时间戳判断。
  • 处理删除事件要小心:文件被删除后,对应的 watch 描述符就失效了。记得从监控列表中移除,否则下次 read 会报错。
  • 递归监控要自己实现:两个平台都不自动监控子目录。我习惯用一个哈希表维护所有子目录的 watch 描述符,新增子目录时自动添加监控。

重要提醒:文件监控不是万能的。它只能告诉你「文件变了」,但变的是什么内容,还得你自己去读。另外,监控大量文件时,事件风暴可能导致程序 CPU 飙升——我见过一个监控 5 万个文件的程序,日志轮转时瞬间产生几千个事件,直接把程序卡死了。解决方案是加一个节流阀,比如 100ms 内同一个文件的事件只处理一次。

好了,文件监控这块就聊到这儿。记住一句话:能用系统 API 就别自己轮询,省心省力还高效。


公众号:蓝海资料掘金营,微信 deep3321