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

  1. inotify_init1,初始化inotify fd;
  2. inotify_add_watch,监控某个文件/目录;
  3. receive event,处理文件变动事件;
  4. inotify_rm_watch 删除监控

inotify 的 fd 也实现了 poll 操作,所以可以注册到 epoll 里面,如上面的例子所示。

需要注意的是 inotify 可能会持有 inode 的锁,还会去缓存 dentry 等,所以如果这个文件夹里的文件特别多可能会有问题。

原理

image
上图是一个基本原理图(图来自: https://www.qiyacloud.cn/2021/08/2021-08-22/ ),我们根据这个解释一下流程

初始化创建

使用inotify_init1创建,内核里,inotify_init1() 做了几件关键的事:

  1. 创建一个 fsnotify_group,可以理解为“这一个 inotify 实例的事件中心”;
  2. 创建一个匿名 inode + struct file
  3. 把这个 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_createfsnotify_unlinkfsnotify_modify 等)最后都会走到一个统一入口:fsnotify(inode, mask, data, data_is, file_name, cookie);
这个 fsnotify() 做了几件事:

  1. 根据本次事件的 inodemask,找到所有对这个 inode/路径注册过 watch 的 fsnotify_group
  2. 对每个 group 调用 send_to_group()
    • group->ops->handle_event(...)(对于 inotify 来说,就是 inotify_handle_event());
  3. 把这次事件封装成 fsnotify_event,塞到 group 的事件队列;
  4. 最后调用:fsnotify_add_event(...)-> wake_up(&group->notification_waitq);

等待与唤醒

关键在于 notification_waitq 这个等待队列。

  • 任何想等待 inotify 事件的人(进程/线程),都会通过 poll/select/epoll 调用 inotify fd 的 .poll 回调。
  • inotify fd 的 .poll 实现是 inotify_poll(),里面会做两件事:
    1. 如果当前 group 事件队列非空,直接返回 EPOLLIN,表示 fd 已经可读;
    2. 如果还没有事件,就调用:poll_wait(file, &group->notification_waitq, wait);
      这会把当前任务包装成一个 wait_queue_entry,挂到 notification_waitq 的链表上,然后在上层(如 epoll_wait)通过 schedule() 进入睡眠。

wake_up(&group->notification_waitq) 做的事情就是:

  1. 遍历这个等待队列上的所有 wait_queue_entry
  2. 找到每个 entry 对应的 task_struct(一般挂在 entry->private 里);
  3. 把这些 task 的状态从 TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE 改成 TASK_RUNNING
  4. 把它们从 waitqueue 链表里摘掉,塞回调度器的运行队列。

这样:

  • 没有事件时:线程睡在 group->notification_waitq 上;
  • 有事件时:wake_up 把线程标记为可运行,调度器切回来,线程从 poll/epoll_wait/read 的阻塞处醒来,继续往下执行。

epoll 如何参与这条链路?
很多时候我们不会直接在 inotify fd 上 read-blocking,而是把它丢进 epoll,像上面那段代码一样。
这里发生了两层等待/唤醒:

  1. 底层: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
  2. 上层:epoll自己的 waitqueue
    • epoll 自身也有一个 ep->wq
    • 当所有 fd 的 .poll 都没返回可读时,epoll_wait() 会在 ep->wq 上再睡一层;
    • 一旦某个 fd 的 .poll 将它标为 ready,epoll 会把这个 fd 放进自己的 ready list,然后 wake_up(&ep->wq)epoll_wait 这个系统调用返回到用户态。
      你可以把整个链路看成:
  • 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 把事件列表返回给用户态。
posted @ 2026-04-24 10:10  uran0sh  阅读(30)  评论(0)    收藏  举报