fsnotify-监控linux文件系统
Linux 内核提供了 fsnotify 用于监控文件的变动。
使用
在用户态主要通过 inotify 来使用,比如:
#include <sys/inotify.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>
#include <iostream>
#include <vector>
int main() {
int inofd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (inofd < 0) {
perror("inotify_init1");
return 1;
}
int wd = inotify_add_watch(inofd, "/tmp/watch", IN_CREATE | IN_DELETE | IN_MODIFY | IN_CLOSE_WRITE);
int epfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev{};
ev.events = EPOLLIN;
ev.data.fd = inofd;
epoll_ctl(epfd, EPOLL_CTL_ADD, inofd, &ev);
std::vector<char> buf(4096);
struct epoll_event events[10];
while (true) {
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == inofd) {
ssize_t len = read(inofd, buf.data(), buf.size());
if (len <= 0) continue;
for (char* ptr = buf.data(); ptr < buf.data() + len;) {
auto* e = reinterpret_cast<struct inotify_event*>(ptr);
std::cout << "Event on wd=" << e->wd << " ";
if (e->len) {
std::cout << "file =" << e->name;
}
std::cout << std::endl;
ptr += sizeof(*e) + e->len;
}
}
}
}
}
inotify使用的方式大概如下,具体可参考:Linux man page inotify.7
inotify_init1,初始化inotify fd;inotify_add_watch,监控某个文件/目录;- receive event,处理文件变动事件;
inotify_rm_watch删除监控
inotify 的 fd 也实现了 poll 操作,所以可以注册到 epoll 里面,如上面的例子所示。
需要注意的是 inotify 可能会持有 inode 的锁,还会去缓存 dentry 等,所以如果这个文件夹里的文件特别多可能会有问题。
原理

上图是一个基本原理图(图来自: https://www.qiyacloud.cn/2021/08/2021-08-22/ ),我们根据这个解释一下流程
初始化创建
使用inotify_init1创建,内核里,inotify_init1() 做了几件关键的事:
- 创建一个
fsnotify_group,可以理解为“这一个 inotify 实例的事件中心”; - 创建一个匿名 inode +
struct file; - 把这个
fsnotify_group *挂到file->private_data上。
于是我们有了这样的一条链路:
- 用户空间拿到的
inofd→ 内核对应一个struct file; file->private_data指向一个struct fsnotify_group;- 这个
fsnotify_group里有:- 事件队列(链表,缓存
fsnotify_event); - 一个
wait_queue_head_t notification_waitq,用于睡眠/唤醒等待这个 inotify 事件的任务; - 一组回调
group->ops,针对不同后端(inotify/fanotify)的处理逻辑。
- 事件队列(链表,缓存
事件采集
文件的所有操作,最终都会走到 VFS 层(vfs_read / vfs_write / vfs_unlink / vfs_rename 等)。
fsnotify 的设计正是:在 VFS 这一层统一“埋点”,采集文件操作事件。
以读/写为例,内核代码大致如下(简化理解):
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
// ...
ret = __vfs_read(file, buf, count, pos);
if (ret > 0) {
// 事件采集点:访问事件
fsnotify_access(file);
}
}
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
// ...
ret = __vfs_write(file, buf, count, pos);
if (ret > 0) {
// 事件采集点:修改事件
fsnotify_modify(file);
}
}
各种 fsnotify_xxx()(fsnotify_create、fsnotify_unlink、fsnotify_modify 等)最后都会走到一个统一入口:fsnotify(inode, mask, data, data_is, file_name, cookie);
这个 fsnotify() 做了几件事:
- 根据本次事件的
inode和mask,找到所有对这个 inode/路径注册过 watch 的fsnotify_group - 对每个 group 调用
send_to_group():group->ops->handle_event(...)(对于 inotify 来说,就是inotify_handle_event());
- 把这次事件封装成
fsnotify_event,塞到 group 的事件队列; - 最后调用:
fsnotify_add_event(...)-> wake_up(&group->notification_waitq);
等待与唤醒
关键在于 notification_waitq 这个等待队列。
- 任何想等待 inotify 事件的人(进程/线程),都会通过
poll/select/epoll调用 inotify fd 的.poll回调。 - inotify fd 的
.poll实现是inotify_poll(),里面会做两件事:- 如果当前 group 事件队列非空,直接返回
EPOLLIN,表示 fd 已经可读; - 如果还没有事件,就调用:poll_wait(file, &group->notification_waitq, wait);
这会把当前任务包装成一个wait_queue_entry,挂到notification_waitq的链表上,然后在上层(如epoll_wait)通过schedule()进入睡眠。
- 如果当前 group 事件队列非空,直接返回
wake_up(&group->notification_waitq) 做的事情就是:
- 遍历这个等待队列上的所有
wait_queue_entry; - 找到每个 entry 对应的
task_struct(一般挂在entry->private里); - 把这些 task 的状态从
TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE改成TASK_RUNNING; - 把它们从 waitqueue 链表里摘掉,塞回调度器的运行队列。
这样:
- 没有事件时:线程睡在
group->notification_waitq上; - 有事件时:
wake_up把线程标记为可运行,调度器切回来,线程从poll/epoll_wait/read的阻塞处醒来,继续往下执行。
epoll 如何参与这条链路?
很多时候我们不会直接在 inotify fd 上 read-blocking,而是把它丢进 epoll,像上面那段代码一样。
这里发生了两层等待/唤醒:
- 底层:inotify 自己的 waitqueue
epoll_wait()内部会对每个关注的 fd 调用它的.poll;- 对
inofd来说,就是调用inotify_poll(); inotify_poll()发现当前没有事件,就调用poll_wait(file, &group->notification_waitq, wait);- 于是 epoll 线程被挂到
group->notification_waitq上,等待 fsnotify 的wake_up。
- 上层:epoll自己的 waitqueue
- epoll 自身也有一个
ep->wq; - 当所有 fd 的
.poll都没返回可读时,epoll_wait()会在ep->wq上再睡一层; - 一旦某个 fd 的
.poll将它标为 ready,epoll 会把这个 fd 放进自己的 ready list,然后wake_up(&ep->wq)让epoll_wait这个系统调用返回到用户态。
你可以把整个链路看成:
- epoll 自身也有一个
- VFS:文件事件发生 →
fsnotify_*→fsnotify()→fsnotify_add_event()→ 往fsnotify_group的事件队列里塞一条记录 →wake_up(&group->notification_waitq); - inotify:负责把 group 里的事件变成 inotify fd 的“可读状态”(通过
.poll&read); - epoll:负责汇总多个 fd 的
.poll状态,一旦有任意一个 ready,就通过epoll_wait把事件列表返回给用户态。

浙公网安备 33010602011771号