当前位置:首页 > 文章列表 > 文章 > linux > Linux fanotify怎样监控目录变化并过滤事件

Linux fanotify怎样监控目录变化并过滤事件

来源:17golang原创 2026-11-05 02:32:43 0浏览 收藏

我第一次把目录监控从轮询改成事件通知时,最容易写错的不是宏名称,而是把“目录被标记”误解成“整棵目录树都被递归监听”。Linux fanotify 的基本链路其实很清楚:fanotify_init() 创建事件队列,fanotify_mark() 把目录、挂载点或文件系统加入监控范围,用户态再从返回的文件描述符读取事件,并用 mask 过滤自己真正关心的变化。

本文使用通知类事件做一个最小示例,目标是捕获目录下的创建、移动到目录和写入关闭事件。官方资料入口可以从 https://www.kernel.org/doc/html/latest/ 开始,具体 fanotify 接口还可以查看 https://man7.org/linux/man-pages/man7/fanotify.7.html。示例不会对文件访问做放行或拒绝,也不会把它包装成安全审计系统。

想监控一个目录并过滤事件,核心顺序是“初始化队列—添加目录标记—读取 metadata—按 mask 分流—关闭事件 fd”。如果需要覆盖子目录,单独标记父目录并不够,应重新评估挂载点或文件系统级标记。
  • 初始化:用 fanotify_init 获得事件队列文件描述符。
  • 标记:用 fanotify_mark 指定目录和关心的事件掩码。
  • 过滤:只处理 FAN_CREATE、FAN_MOVED_TO、FAN_CLOSE_WRITE 等目标事件。
  • 清理:通知里的文件描述符由接收程序负责关闭,队列溢出和非递归范围也要单独处理。

Linux fanotify 的事件链路

fanotify 不是一个“每次变化都回调一个函数”的高层库,而是一个由内核维护事件队列的文件描述符接口。调用 fanotify_init 后得到的 fd 同时承担两件事:传给 fanotify_mark 作为通知组的句柄,以及在循环里作为 read 的来源。

标记时可以选择目录、挂载点或文件系统,并用事件掩码声明关注哪些动作。下面的示意图把这条链路拆成了四段;它是原创结构图,不是终端或官方页面截图。

Linux fanotify 从目录事件进入内核队列并由用户态过滤的结构示意图
图1:Linux fanotify 从目录事件进入内核队列,再由用户态按事件掩码筛选的结构示意图。

先用通知类事件标记一个目录

下面的 C 示例把 /tmp/watch-demo 作为命令行参数,使用 FAN_MARK_ONLYDIR 限定目标是目录,并监听三类常见变化。因为 FAN_CREATE 和 FAN_MOVED_TO 属于需要文件句柄信息的目录项事件,初始化时加上 FAN_REPORT_DFID_NAME,标记时再加上 FAN_EVENT_ON_CHILD 与 FAN_ONDIR。FAN_NONBLOCK 让示例在没有事件时不会永久卡住,便于把它放入已有的轮询循环;正式守护进程也可以结合 poll 或 epoll 等待 fd 可读。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "用法:%s 目录\n", argv[0]);
        return EXIT_FAILURE;
    }

    /* 创建带目录项信息的通知队列;非阻塞便于接入 poll/epoll。 */
    int fan_fd = fanotify_init(FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_NONBLOCK |
                               FAN_REPORT_DFID_NAME,
                               O_RDONLY | O_LARGEFILE);
    if (fan_fd == -1) {
        perror("fanotify_init");
        return EXIT_FAILURE;
    }

    /* 只标记目录本身,监听其直接子项的创建、移动和写入关闭。 */
    uint64_t mask = FAN_CREATE | FAN_MOVED_TO | FAN_CLOSE_WRITE |
                    FAN_EVENT_ON_CHILD | FAN_ONDIR;
    if (fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_ONLYDIR,
                      mask, AT_FDCWD, argv[1]) == -1) {
        perror("fanotify_mark");
        close(fan_fd);
        return EXIT_FAILURE;
    }

    printf("开始监听:%s\n", argv[1]);
    char buffer[8192];
    for (;;) {
        ssize_t length = read(fan_fd, buffer, sizeof(buffer));
        if (length == -1) {
            /* 非阻塞队列暂时为空时,交给上层循环等待下一次可读。 */
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                usleep(100000);
                continue;
            }
            if (errno == EINTR) {
                continue;
            }
            perror("read");
            break;
        }

        struct fanotify_event_metadata *meta;
        for (meta = (struct fanotify_event_metadata *)buffer;
             FAN_EVENT_OK(meta, length);
             meta = FAN_EVENT_NEXT(meta, length)) {
            /* 队列溢出没有可用对象 fd,先单独记录这个异常信号。 */
            if (meta->mask & FAN_Q_OVERFLOW) {
                fprintf(stderr, "警告:fanotify 事件队列溢出\n");
                continue;
            }

            uint64_t wanted = FAN_CREATE | FAN_MOVED_TO | FAN_CLOSE_WRITE;
            if ((meta->mask & wanted) == 0) {
                /* 其他事件不进入业务处理,但仍要释放它携带的 fd。 */
                if (meta->fd >= 0) close(meta->fd);
                continue;
            }

            printf("命中事件:mask=0x%llx pid=%d fd=%d\n",
                   (unsigned long long)meta->mask, meta->pid, meta->fd);

            /* FID 模式通常返回 FAN_NOFD;若改用 fd 模式,必须关闭有效对象 fd。 */
            if (meta->fd >= 0) close(meta->fd);
        }
    }

    close(fan_fd);
    return EXIT_FAILURE;
}

编译时可以使用 gcc -O2 -Wall -Wextra fanwatch.c -o fanwatch,再执行 ./fanwatch /tmp/watch-demo。命令只说明构建方式,是否能成功取决于本机内核配置、权限和头文件环境;这里的关键不在轮询间隔,而在每次 read 后用 FAN_EVENT_OK 和 FAN_EVENT_NEXT 正确遍历一个缓冲区中的多条事件。

按 mask 过滤,并及时关闭事件 fd

一条 fanotify 事件至少包含长度、版本、掩码、对象 fd 和触发进程 pid 等元数据。掩码可能同时包含多个 bit,所以不要把它当成单一枚举值直接比较;更稳妥的做法是使用位运算判断关心的 bit,再根据业务决定是否继续处理。

示例中的过滤顺序有两个容易忽略的点。第一,FAN_Q_OVERFLOW 表示队列曾经溢出,之前的部分事件可能已经丢失,不能把它当成普通文件变化。第二,即使某个事件最终被过滤掉,只要 metadata 中带有有效 fd,接收方仍然应该关闭它。下面的图把这两个分支和清理动作画在了一起。

fanotify event metadata 按 mask 分流并关闭文件描述符的流程示意图
图2:读取 fanotify_event_metadata 后按 mask 分流,并在处理结束时关闭事件文件描述符的流程示意图。
事件或字段处理建议容易误判的地方
FAN_CREATE记录目录下新建对象的通知父目录标记不等于对子目录递归监听
FAN_MOVED_TO关注对象被移动到目标目录需要结合业务判断重命名和跨目录移动
FAN_CLOSE_WRITE在写入 fd 关闭时触发后续处理它不是“每一次写系统调用”通知
FAN_Q_OVERFLOW记录丢失风险并安排补偿扫描不能把队列溢出后的通知当作完整变化集
metadata.fd处理完毕后关闭有效 fd关闭的是事件携带的 fd,不只是 fanotify 主 fd

目录监控不递归,什么时候改成挂载点

这是我实际使用时最先需要写进设计说明的一条边界:fanotify 对一个目录加标记,不会自动把所有子目录都变成监控对象。只监听父目录时,直接在该目录创建或移动对象可以进入事件流,但子目录内部的变化不一定会按你预期出现。

如果业务确实要覆盖目录树,可以在新建子目录时继续添加标记,但这个补标记过程存在竞态窗口;另一条路线是选择合适的挂载点或文件系统级标记,让内核从更大的范围观察事件。范围变大后,事件数量、权限要求和过滤压力也会一起上升,不能只把 FAN_MARK_ONLYDIR 换成更大的范围就结束。

此外,fanotify 只报告通过文件系统 API 触发的部分活动,网络文件系统远端变化、通过 mmap、msync、munmap 产生的修改也有覆盖边界。部署前应明确“要发现的是目录项变化、文件写入结束,还是完整的数据变化”,再决定 fanotify 是否合适。

和 inotify 怎么选

我会把两者的选择理解成监控范围和事件对象模型的取舍,而不是简单的“谁更新”。inotify 更适合从目录项和路径事件出发的小型工具;fanotify 更适合需要挂载点或文件系统范围、并且希望通过事件 fd 访问被通知对象的场景。fanotify 还提供访问权限事件,但权限类事件需要接收方写回允许或拒绝结果,错误处理和延迟风险都会更高。

如果只是做一个配置目录变化提醒,先用较小监控范围和简单过滤即可;如果要做内容扫描、文件访问控制或大树范围监控,应额外设计队列积压、权限、补偿扫描、资源限制和失败降级。监控接口能告诉你“发生了什么”,不自动保证你已经拿到了完整、无重复、无丢失的业务事实。

常见问题

fanotify 能直接递归监听目录吗?

给父目录添加普通标记并不会自动递归。可以为新建子目录继续加标记,但补标记有竞态;需要覆盖整棵树时,应评估挂载点或文件系统级监控以及随之增加的事件量。

为什么 read 读到的不是一条固定大小的事件?

一次 read 可能返回多个连续的事件记录,每条记录由 event_len 描述长度。因此应使用 FAN_EVENT_OK 和 FAN_EVENT_NEXT 按记录遍历,而不是按固定结构大小递增。

过滤掉事件后还要 close metadata.fd 吗?

要。只要它不是 FAN_NOFD 或其他无效值,就应在处理或忽略后关闭事件携带的 fd;否则长时间运行的监听器可能出现文件描述符泄漏。

队列溢出后能不能继续当作正常监听?

可以继续运行,但不能假设溢出期间的变化都已收到。应记录 FAN_Q_OVERFLOW,安排一次补偿扫描或重建索引,并根据业务容忍度决定是否暂停后续处理。

fanotify 需要特殊权限吗?

所需权限取决于标记范围、事件类型、内核配置和系统安全策略。示例只展示接口关系,不承诺普通用户在所有发行版上都能标记任意目录;遇到 EPERM、EINVAL 等错误时,应先查看当前内核和目标对象的限制。

总结一下:用 fanotify 监控目录时,先把监控范围、事件掩码和通知类型定清楚,再写用户态过滤器。最小可靠实现至少要能处理多事件缓冲区、无关事件、队列溢出和 fd 清理;如果需求已经扩大到递归目录树或完整变更审计,就不要把一个父目录标记当成全部答案。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Linux io_uring multishot accept怎样持续接收连接事件Linux io_uring multishot accept怎样持续接收连接事件
上一篇
Linux io_uring multishot accept怎样持续接收连接事件
Linux pidfd_send_signal怎样向指定进程安全发送信号
下一篇
Linux pidfd_send_signal怎样向指定进程安全发送信号
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    888次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    887次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    970次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    955次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    782次使用