Linux fanotify怎样监控目录变化并过滤事件
我第一次把目录监控从轮询改成事件通知时,最容易写错的不是宏名称,而是把“目录被标记”误解成“整棵目录树都被递归监听”。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 的来源。
标记时可以选择目录、挂载点或文件系统,并用事件掩码声明关注哪些动作。下面的示意图把这条链路拆成了四段;它是原创结构图,不是终端或官方页面截图。

先用通知类事件标记一个目录
下面的 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,接收方仍然应该关闭它。下面的图把这两个分支和清理动作画在了一起。

| 事件或字段 | 处理建议 | 容易误判的地方 |
|---|---|---|
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 清理;如果需求已经扩大到递归目录树或完整变更审计,就不要把一个父目录标记当成全部答案。
Linux io_uring multishot accept怎样持续接收连接事件
- 上一篇
- Linux io_uring multishot accept怎样持续接收连接事件
- 下一篇
- Linux pidfd_send_signal怎样向指定进程安全发送信号
-
- 文章 · linux | 52分钟前 | Linux timeout 防火墙规则 nftables sets flags timeout 临时封禁
- Linux nftables sets timeout怎样让临时规则自动过期
- 134浏览 收藏
-
- 文章 · linux | 55分钟前 |
- Linux ethtool ring怎样查看网卡收发环大小
- 264浏览 收藏
-
- 文章 · linux | 57分钟前 |
- Linux ip route get 怎样按源地址和网卡确认内核选路
- 171浏览 收藏
-
- 文章 · linux | 59分钟前 | Linux · 服务管理 · Linux 退出码 Restart systemd.service RestartPreventExitStatus
- Linux systemd.service RestartPreventExitStatus 怎样让指定退出码不重启
- 371浏览 收藏
-
- 文章 · linux | 1小时前 |
- Linux systemd.exec OOMScoreAdjust怎样调整服务被 OOM 选择的优先级
- 269浏览 收藏
-
- 文章 · linux | 1小时前 |
- Linux clone3 set_tid怎样为容器初始化进程设置线程 ID
- 119浏览 收藏
-
- 文章 · linux | 1小时前 | Linux 路径安全 openat2 RESOLVE_BENEATH
- Linux openat2 RESOLVE_BENEATH怎样限制路径不能逃逸根目录
- 208浏览 收藏
-
- 文章 · linux | 1小时前 |
- Linux pidfd_send_signal怎样向指定进程安全发送信号
- 423浏览 收藏
-
- 文章 · linux | 1小时前 | 网络编程 · Linux ·
- Linux io_uring multishot accept怎样持续接收连接事件
- 415浏览 收藏
-
- 文章 · linux | 1小时前 |
- Linux io_uring registered files怎样减少高频 I/O 的文件描述符开销
- 120浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 888次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 887次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 970次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 955次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 782次使用
-
- Linux vmstat 如何分辨内存抖动与磁盘等待:si、so、wa 与复测顺序
- 2026-08-30 501浏览
-
- Linux搭建vsftpdFTP服务器教程
- 2026-04-30 501浏览
-
- Shell脚本安装教程:.sh一键安装指南
- 2026-03-16 501浏览
-
- Linux清空文件内容的几种方法
- 2025-12-01 501浏览
-
- Linux命令行下载文件技巧
- 2025-11-23 501浏览
