文件系统监控: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;
}
小提示:inotify 默认是阻塞读取的。如果你不想阻塞,可以用 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;
}
注意:Windows 的 ReadDirectoryChangesW 有一个经典坑——缓冲区溢出。如果短时间内大量文件变化,缓冲区不够大,API 会返回失败。我建议缓冲区至少设 64KB,生产环境最好 128KB 以上。另外,异步模式(配合 I/O 完成端口)在高并发场景下更靠谱。

跨平台对比

特性 inotify (Linux) ReadDirectoryChangesW (Windows)
事件粒度 细粒度,支持多种事件 较粗,部分事件需组合判断
子目录监控 需递归添加 watch bWatchSubtree 参数直接支持
缓冲区管理 内核管理,用户只需 read 用户自己管理,容易溢出
异步支持 配合 epoll/select 配合 I/O 完成端口
最大监控数 受 /proc/sys/fs/inotify/max_user_watches 限制 受系统资源限制

文件变化通知的通用设计模式

不管用哪个平台,文件监控的架构其实差不多。我画了一张图,帮你理清思路:

文件变化通知通用架构 应用层回调 事件分发/队列 平台抽象层 inotify (Linux) ReadDirectoryChangesW (Windows) 其他平台实现 用户代码 事件排队、去重、合并 统一接口,屏蔽平台差异 平台原生 API

这个架构的核心思想是:上层不关心底层是 inotify 还是 ReadDirectoryChangesW。我在一个跨平台文件同步工具里就是这么设计的,上层只注册回调,底层自动选择平台实现。这样换平台时,只需要改底层那一小块代码。

避坑指南

我曾经踩过的坑,分享给你:

  • 事件丢失:inotify 的环形缓冲区满了会丢事件。解决办法是增大 /proc/sys/fs/inotify/max_user_watchesmax_queued_events
  • 重复事件:一个文件修改可能触发多次 IN_MODIFY。我一般加一个去重逻辑,比如 100ms 内同一文件只处理一次。
  • 目录监控的递归问题:inotify 不会自动监控子目录,需要自己递归添加 watch。Windows 的 bWatchSubtree 虽然方便,但性能开销大,监控大目录时要小心。
  • 文件句柄泄漏:Windows 上 CreateFile 打开的目录句柄,用完一定要 CloseHandle。我见过线上服务因为句柄泄漏,最后把系统资源耗光了。

总结

文件系统监控,说白了就是让程序具备“感知”文件变化的能力。Linux 用 inotify,Windows 用 ReadDirectoryChangesW,两者各有千秋。我个人建议,跨平台项目最好封装一层抽象接口,把平台差异隔离掉。

嗯,这一章就聊到这儿。记住,文件监控不是银弹——它解决的是“实时感知”的问题,但事件处理的可靠性、性能、资源管理,都需要你仔细设计。下次写文件监控代码时,想想我提到的那些坑,能省不少调试时间。


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