背景#
Linux 上想监控文件系统事件,大多数人第一反应是 inotify。inotify_add_watch() 给目录加个 watch,然后 read() 等事件来。
但 inotify 有几个让人抓狂的限制。
第一,你得逐个目录加 watch。 想监控整个 /home?先递归遍历所有子目录,每个都加一个 watch。光遍历就要命,更别提文件和目录随时在创建和删除,watch 永远赶不上变化。目录刚创建完,inotify 还没来得及加 watch,文件已经写进去了。
第二,inotify 没有 PID。 你知道有人写了文件,但不知道是谁写的。想追踪进程,只能从 /proc 倒查,这是个经典的 TOCTOU 竞态。
第三,inotify 只能事后通知。 等 IN_CLOSE_WRITE 到达用户态,数据早就落盘了。想在文件被读取前拦截,不可能。
fanotify 就是来解决这三个问题的。它 2010 年随 Linux 2.6.36 合入主线,最初只有一个用途:给杀毒软件做 on-access 扫描。但十几年下来,fanotify 长出了大量新能力:create/delete/move 事件、文件路径解析、甚至能直接在用户态决定一个 open() 是放行还是拒绝。
核心原理#
fanotify 和 inotify 的根本区别在于监控粒度。
inotify 监控的是单个 inode,你得显式告诉内核"帮我看着这个目录"。fanotify 监控的是挂载点或整个文件系统,一句 fanotify_mark() 搞定一切。
三步走模型#
fanotify 的使用流程很简单:
| |
其中有几个关键设计。
通知组(notification group): fanotify_init() 返回的 fd 是个内核对象,所有注册的 mark 共享同一个事件队列。多个进程可以各自创建通知组监控同一个文件系统,互不干扰。
mark 的 masks 是双层的: 每个 mark 维护两个 bitmask,mark mask(要产生什么事件)和 ignore mask(抑制什么事件)。这个设计让缓存类应用可以精细控制事件流:文件第一次被修改时通知,之后在 close 前都忽略。
权限事件有超能力: 当接到 FAN_OPEN_PERM 时,发起 open() 的进程被内核暂停,等你用 write() 回一个 FAN_ALLOW 或 FAN_DENY。选后者,open() 直接返回 EPERM,文件根本没被打开。
三种通知级别#
fanotify_init() 的 flags 里有一个必选的 class 参数:
| class | 能拦截? | 典型用途 |
|---|---|---|
FAN_CLASS_NOTIF | 否 | 审计日志、文件同步 |
FAN_CLASS_CONTENT | 是 | 杀毒扫描、DLP |
FAN_CLASS_PRE_CONTENT | 是 | 分级存储管理(HSM) |
FAN_CLASS_PRE_CONTENT 最特殊。一个挂载点只能有一个进程持有。它在其他 class 之前收到事件,常用于在文件内容最终确定前做预处理。我们日常写监控工具选 FAN_CLASS_NOTIF 就够了。
FAN_MARK_MOUNT vs FAN_MARK_FILESYSTEM#
用 fanotify_mark() 注册目标时,有两个标记决定了监控范围:
| 标记 | 范围 | 内核要求 |
|---|---|---|
FAN_MARK_MOUNT | 单个挂载点下的所有文件和目录 | 所有支持 fanotify 的内核 |
FAN_MARK_FILESYSTEM | 整个文件系统,包括所有挂载点 | Linux 4.20+ |
FAN_MARK_MOUNT 是你最常见的用法。给 / 加个 mark,根挂载点下的所有文件操作都会产生事件。但 /proc、/sys 等独立挂载点不受影响。
FAN_MARK_FILESYSTEM 更强。比如你给 /usr 加 mark,就算 /usr 后来被 bind mount 到别处,事件照常产生。适合"一网打尽"的场景。
事件结构体拆解#
从 fanotify fd 读出来的数据长这样:
| |
几个注意点:
fd如果不等于FAN_NOFD,你必须 close 它,否则泄漏。内核给这个 fd 打了FMODE_NONOTIFY标记,读写它不会触发新一轮 fanotify 事件(避免递归死锁)。- 如果
event_len > metadata_len,说明后面还跟着附加信息记录,比如用FAN_REPORT_DFID_NAME时会有文件路径信息。 - 一次
read()可能返回多个事件,用FAN_EVENT_OK()和FAN_EVENT_NEXT()宏遍历。
关键事件一览#
fanotify 支持的事件比 inotify 多得多:
| 事件 | 含义 | 能否拦截 |
|---|---|---|
FAN_OPEN | 文件/目录被打开 | 是(FAN_OPEN_PERM) |
FAN_ACCESS | 文件被读取 | 是(FAN_ACCESS_PERM) |
FAN_OPEN_EXEC | 文件被执行 | 是(FAN_OPEN_EXEC_PERM) |
FAN_MODIFY | 文件内容被修改 | 否 |
FAN_CLOSE_WRITE | 可写文件被关闭 | 否 |
FAN_CREATE | 子文件或目录被创建 | 否 |
FAN_DELETE | 子文件或目录被删除 | 否 |
FAN_MOVED_FROM / _TO | 文件被移入/移出 | 否 |
FAN_OPEN_EXEC_PERM 是个被低估的安全工具。它在 execve() 加载新进程镜像之前触发,你可以用它实现用户态的应用白名单,不需要写一行 LSM 内核模块。
代码实战#
下面写一个完整的监控程序,跑起来就能看到系统上谁在读什么文件。
初始化#
| |
事件循环#
| |
编译运行#
| |
输出会是滚屏的实时事件:
| |
权限拦截版(拦截所有未授权的 exec)#
把 FAN_CLASS_NOTIF 换成 FAN_CLASS_CONTENT,加几行 write() 响应:
| |
把 FAN_DENY 改成条件判断(比如只允许 /usr/bin/ 下的文件执行),就是一个用户态应用白名单。
生态现状#
fanotify 早已不是实验室玩具。以下项目直接依赖它:
| 项目 | 用途 | 使用的 fanotify 特性 |
|---|---|---|
| ClamAV | on-access 病毒扫描 | FAN_CLASS_CONTENT + FAN_OPEN_PERM |
| fapolicyd(RHEL) | 应用完整性验证与白名单 | FAN_OPEN_EXEC_PERM |
| CrowdStrike / SentinelOne | Linux EDR agent | 全事件掩码 + FAN_REPORT_DFID_NAME |
| systemd-journald | 日志转发 | FAN_MODIFY 监控 journal 文件 |
| BCC / bpftrace | 可观测性集成 | fanotify + eBPF 混合方案 |
RHEL 的 fapolicyd 值得单独提一下。它用 FAN_OPEN_EXEC_PERM 在 execve() 前校验可执行文件的 SHA256 哈希和 RPM 签名,不通过的进程直接 FAN_DENY。这一套全程用户态,没碰一行 LSM 代码。
ClamAV 的 on-access 扫描是另一个典型用例。打开文件时,fanotify 把调用者挂起,ClamAV 拿到 fd 扫描内容,干净则 FAN_ALLOW,有毒则 FAN_DENY。用户看到的只是 open() 慢了 50ms,而不是文件被感染。
从内核版本来看,fanotify 还在快速演进:5.1 加了 create/delete/move 事件和 FAN_REPORT_DFID_NAME,5.9 加了 FAN_REPORT_NAME 简化路径获取,6.14 加了 FAN_REPORT_MNT 支持 mount namespace 标记。这个 API 远没到稳定期。
今日可执行动作#
- 跑一下上面的监控代码。把完整程序编译运行 10 秒,看看你的系统在这一小段窗口里产生了多少文件事件。你会被震惊的。
- 写一个拦截器只放行
/usr/bin/下的二进制执行,其余FAN_DENY。验证效果:sudo ./fmon_block后再开个终端,很多命令应该挂掉。 - 阅读
samples/fanotify/fs-monitor.c,内核源码树里的官方示例,展示了FAN_FS_ERROR的用法。用git clone --depth=1 https://github.com/torvalds/linux拉一份源码就能看到。
参考#
- fanotify(7) - Arch manual pages
- fanotify_init(2) - Linux manual page
- fanotify_mark(2) - Linux manual page
- Linux File Monitoring With Fanotify - Mathscantor’s Blog (2025)
- Linux fanotify for Real-Time Filesystem Security Monitoring - systemshardening.com (2026)
- File System Monitoring with fanotify - Linux Kernel docs

