监听容器中的文件系统事件
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 的工作流程如下:
用户通过系统调用(如:write、read)操作文件;
内核将文件系统事件保存到 fsnotify_group 的事件队列中;
唤醒等待 inotify 的进程(listener);
进程通过 fd 从内核队列读取 inotify 事件。
其中,inotify_event_info 的定义如下:
struct inotify_event_info { struct fsnotify_event fse; u32 mask; /* Watch mask. */ int wd; /* Watch descriptor. */ u32 sync_cookie; /* Cookie to synchronize two events. */ int name_len; /* Name. */ char name[]; /* Length (including NULs) of name. */};
mask 标记具体的文件操作事件。
API 介绍Inotify 可以用来监听单个文件,也可以用来监听目录。当监听的是目录时,inotify 除了生成目录的事件,还会生成目录中文件的事件。
“
注意:当使用 inotify 监听目录时,并不会递归监听子目录中的文件,如果需要得到这些事件,需要手动指定监听这些文件。对于很大的目录树,这个过程将花费大量时间。
参考:inotify.7[3]
inotify_init(void)
初始化 inotify 实例,返回文件描述符,用于内核向用户态程序传输监听到的 inotify 事件。函数声明为:
int inotify_init(void);
内核同时提供了int inotify_init1(int flags),flags 的可选值如下:
IN_NONBLOCK 读取文件描述符时不会被阻塞,即使没有数据可用也是如此。 如果没有数据可用,则读操作将立即返回0,而不是等待数据可用。IN_CLOEXEC 如果在程序运行时打开了一个文件描述符,并且在调用execve()时没有将其关闭, 那么在新程序中仍然可以使用该文件描述符。 设置IN_CLOEXEC标志后,可以确保在调用execve()时关闭文件描述符,避免在新程序中使用。
可以通过 OR 指定多个flag,当flags=0等价于int inotify_init(void)。
inotify_add_watch
添加需要监听的目录或文件(watch list),可以添加新的路径,也可以是已经添加过的路径。fd 是inotify_init返回的文件描述符,mask 指定需要监听的事件类型,通过 OR 指定多个事件。返回值是当前路径的wd(watch descriptor),可用于移除对该路径的监听。
函数声明为:
#include <sys/inotify.h>int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
Inotify 支持监听的事件包括:
/* Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. */#define IN_ACCESS 0x00000001 /* File was accessed. */#define IN_MODIFY 0x00000002 /* File was modified. */#define IN_ATTRIB 0x00000004 /* Metadata changed. */#define IN_CLOSE_WRITE 0x00000008 /* Writtable file was closed. */#define IN_CLOSE_NOWRITE 0x00000010 /* Unwrittable file closed. */#define IN_CLOSE (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) /* Close. */#define IN_OPEN 0x00000020 /* File was opened. */#define IN_MOVED_FROM 0x00000040 /* File was moved from X. */#define IN_MOVED_TO 0x00000080 /* File was moved to Y. */#define IN_MOVE (IN_MOVED_FROM | IN_MOVED_TO) /* Moves. */#define IN_CREATE 0x00000100 /* Subfile was created. */#define IN_DELETE 0x00000200 /* Subfile was deleted. */#define IN_DELETE_SELF 0x00000400 /* Self was deleted. */#define IN_MOVE_SELF 0x00000800 /* Self was moved. */
inotify_rm_watch
移除被监听的路径。fd 是inotify_init返回的文件描述符,wd 是inotify_add_watch返回的监听文件描述符。
函数声明为:
#include <sys/inotify.h>int inotify_rm_watch(int fd, int wd);实例
以下是基于 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(())}
编译&测试:
cargo build./target/debug/inotify test
可以看到,inotify 不会递归监听二级目录下的文件dir1/file2.txt。
经测试,Inotify 可以直接监听容器 rootfs 下的目录:
nerdctl run --rm -it golang./target/debug/inotify /run/containerd/io.containerd.runtime.v2.task/default/CONTAINERD_ID/rootfsFanotify基本介绍
Inotify 能够监听目录和文件的事件,但这种 notifiation 机制也存在局限:inotify 只能通知用户态进程触发了哪些文件系统事件,而无法进行干预,典型的应用场景是杀毒软件。
Fanotify[4] 的出现就是为了解决这个问题,同时允许递归监听目录下的子目录和文件。
Fanotify 的工作流程如下:
用户通过系统调用(如:write、read)操作文件;
内核将文件系统事件发送到 fsnotify_group 的事件队列中;
唤醒等待 fanotify 事件的进程(listener);
进程通过 fd 从内核队列读取 fanotify 事件;
如果是 FAN_OPEN_PERM 和 FAN_ACCESS_PERM 监听类型,进程需要通过 write 把许可信息(允许 or 拒绝)写回内核;
内核根据许可信息决定是否继续完成该文件系统事件。
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。