博客专栏

EEPW首页 > 博客 > 监听容器中的文件系统事件

监听容器中的文件系统事件

发布人:电子禅石 时间:2024-09-18 来源:工程师 发布文章
基本概念

Linux 文件系统事件监听:应用层的进程操作目录或文件时,会触发 system call,此时,内核中的 notification 子系统把该进程对文件的操作事件上报给应用层的监听进程(称为 listerner)。

dnotify:2001 年的 kernel 2.4 版本引入,只能监控 directory,采用的是 signal 机制来向 listener 发送通知,可以传递的信息很有限。

inotify:2005 年在 kernel 2.6.13 中亮相,除了可以监控目录,还可以监听普通文件,inotify 摈弃了 signal 机制,通过 event queue 向 listener 上传事件信息。

fanotify:kernel 2.6.36 引入,fanotify 的出现解决了已有实现只能 notify 的问题,允许 listener 介入并改变文件事件的行为,实现从“监听”到“监控”的跨越。

本文主要介绍如何通过 inotify 和 fanotify 监听容器中的文件系统事件。

Inotify基本介绍

inotify(inode[1] notify)是 Linux 内核中的一个子系统,由 John McCutchan[2] 创建,用于监视文件系统事件。它可以在文件或目录发生变化时通知应用程序,例如,监听文件的创建、修改或删除事件。inotify 可以用于自动更新文件系统视图、重新加载配置文件,记录文件改变历史等场景。

Inotify 的工作流程如下:

  1. 用户通过系统调用(如:write、read)操作文件;

  2. 内核将文件系统事件保存到 fsnotify_group 的事件队列中;

  3. 唤醒等待 inotify 的进程(listener);

  4. 进程通过 fd 从内核队列读取 inotify 事件。

图片

其中,inotify_event_info 的定义如下:

c

mask 标记具体的文件操作事件。

API 介绍

Inotify 可以用来监听单个文件,也可以用来监听目录。当监听的是目录时,inotify 除了生成目录的事件,还会生成目录中文件的事件。

注意:当使用 inotify 监听目录时,并不会递归监听子目录中的文件,如果需要得到这些事件,需要手动指定监听这些文件。对于很大的目录树,这个过程将花费大量时间。

参考:inotify.7[3]

  • inotify_init(void)

初始化 inotify 实例,返回文件描述符,用于内核向用户态程序传输监听到的 inotify 事件。函数声明为:

c

内核同时提供了int inotify_init1(int flags),flags 的可选值如下:

c

可以通过 OR 指定多个flag,当flags=0等价于int inotify_init(void)。

  • inotify_add_watch

添加需要监听的目录或文件(watch list),可以添加新的路径,也可以是已经添加过的路径。fd 是inotify_init返回的文件描述符,mask 指定需要监听的事件类型,通过 OR 指定多个事件。返回值是当前路径的wd(watch descriptor),可用于移除对该路径的监听。

函数声明为:

c

Inotify 支持监听的事件包括:

c
  • inotify_rm_watch

移除被监听的路径。fd 是inotify_init返回的文件描述符,wd 是inotify_add_watch返回的监听文件描述符。

函数声明为:

c
实例

以下是基于 Rust 语言实现的实例:

rust
use nix::{    poll::{poll, PollFd, PollFlags},    sys::inotify::{AddWatchFlags, InitFlags, Inotify, InotifyEvent},};use signal_hook::{consts::SIGTERM, low_level::pipe};use std::os::unix::net::UnixStream;use std::{env, io, os::fd::AsRawFd, path::PathBuf};fn main() -> io::Result<()> {    let args: Vec<String> = env::args().collect();    if args.len() < 2 {        eprintln!("Usage: {} <path>", args[0]);        std::process::exit(1);    }    let path = PathBuf::from(&args[1]);    // 初始化 inotify,得到 fd    let inotify_fd = Inotify::init(InitFlags::empty())?;    // 添加被监听的目录或文件,指定需要监听的事件    let wd = inotify_fd.add_watch(        &path,        AddWatchFlags::IN_ACCESS | AddWatchFlags::IN_OPEN | AddWatchFlags::IN_CREATE,    )?;    let (read, write) = UnixStream::pair()?;    // 注册用于处理信号的 pipe    if let Err(e) = pipe::register(SIGTERM, write) {        println!("failed to set SIGTERM signal handler {e:?}");    }    let mut fds = [        PollFd::new(inotify_fd.as_raw_fd(), PollFlags::POLLIN),        PollFd::new(read.as_raw_fd(), PollFlags::POLLIN),    ];    loop {        match poll(&mut fds, -1) {            Ok(polled_num) => {                if polled_num <= 0 {                    eprintln!("polled_num <= 0!");                    break;                }                if let Some(flag) = fds[0].revents() {                    if flag.contains(PollFlags::POLLIN) {                        // 得到 inotify 事件,进行处理                        let events = inotify_fd.read_events()?;                        for event in events {                            handle_event(event)?;                        }                    }                }                if let Some(flag) = fds[1].revents() {                    if flag.contains(PollFlags::POLLIN) {                        println!("received SIGTERM signal");                        break;                    }                }            }            Err(e) => {                if e == nix::Error::EINTR {                    continue;                }                eprintln!("Poll error {:?}", e);                break;            }        }    }    inotify_fd.rm_watch(wd)?;    Ok(())}fn handle_event(event: InotifyEvent) -> io::Result<()> {    let file_name = match event.name {        Some(name) => name,        None => return Ok(()),    };    let event_mask = event.mask;    let kind = if event_mask.contains(AddWatchFlags::IN_ISDIR) {        "directory"    } else {        "file"    };    println!(        "{} {} was {:?}.",        kind,        file_name.to_string_lossy(),        event_mask    );    Ok(())}

编译&测试:

shell
cargo build./target/debug/inotify test

图片

可以看到,inotify 不会递归监听二级目录下的文件dir1/file2.txt。

经测试,Inotify 可以直接监听容器 rootfs 下的目录:

shell
nerdctl run --rm -it golang./target/debug/inotify /run/containerd/io.containerd.runtime.v2.task/default/CONTAINERD_ID/rootfs

图片

Fanotify基本介绍

Inotify 能够监听目录和文件的事件,但这种 notifiation 机制也存在局限:inotify 只能通知用户态进程触发了哪些文件系统事件,而无法进行干预,典型的应用场景是杀毒软件。

Fanotify[4] 的出现就是为了解决这个问题,同时允许递归监听目录下的子目录和文件。

Fanotify 的工作流程如下:

  1. 用户通过系统调用(如:write、read)操作文件;

  2. 内核将文件系统事件发送到 fsnotify_group 的事件队列中;

  3. 唤醒等待 fanotify 事件的进程(listener);

  4. 进程通过 fd 从内核队列读取 fanotify 事件;

  5. 如果是 FAN_OPEN_PERM 和 FAN_ACCESS_PERM 监听类型,进程需要通过 write 把许可信息(允许 or 拒绝)写回内核;

  6. 内核根据许可信息决定是否继续完成该文件系统事件。

监监听容器中的文件系统事件 - abin在路上 - 博客园 (cnblogs.com)

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。



关键词: fanotify

技术专区

关闭