監(jiān)聽容器中的文件系統(tǒng)事件
Linux 文件系統(tǒng)事件監(jiān)聽:應用層的進程操作目錄或文件時,會觸發(fā) system call,此時,內核中的 notification 子系統(tǒng)把該進程對文件的操作事件上報給應用層的監(jiān)聽進程(稱為 listerner)。
dnotify:2001 年的 kernel 2.4 版本引入,只能監(jiān)控 directory,采用的是 signal 機制來向 listener 發(fā)送通知,可以傳遞的信息很有限。
inotify:2005 年在 kernel 2.6.13 中亮相,除了可以監(jiān)控目錄,還可以監(jiān)聽普通文件,inotify 擯棄了 signal 機制,通過 event queue 向 listener 上傳事件信息。
fanotify:kernel 2.6.36 引入,fanotify 的出現(xiàn)解決了已有實現(xiàn)只能 notify 的問題,允許 listener 介入并改變文件事件的行為,實現(xiàn)從“監(jiān)聽”到“監(jiān)控”的跨越。
本文主要介紹如何通過 inotify 和 fanotify 監(jiān)聽容器中的文件系統(tǒng)事件。
Inotify基本介紹inotify(inode[1] notify)是 Linux 內核中的一個子系統(tǒng),由 John McCutchan[2] 創(chuàng)建,用于監(jiān)視文件系統(tǒng)事件。它可以在文件或目錄發(fā)生變化時通知應用程序,例如,監(jiān)聽文件的創(chuàng)建、修改或刪除事件。inotify 可以用于自動更新文件系統(tǒng)視圖、重新加載配置文件,記錄文件改變歷史等場景。
Inotify 的工作流程如下:
用戶通過系統(tǒng)調用(如:write、read)操作文件;
內核將文件系統(tǒng)事件保存到 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 可以用來監(jiān)聽單個文件,也可以用來監(jiān)聽目錄。當監(jiān)聽的是目錄時,inotify 除了生成目錄的事件,還會生成目錄中文件的事件。
“
注意:當使用 inotify 監(jiān)聽目錄時,并不會遞歸監(jiān)聽子目錄中的文件,如果需要得到這些事件,需要手動指定監(jiān)聽這些文件。對于很大的目錄樹,這個過程將花費大量時間。
參考:inotify.7[3]
inotify_init(void)
初始化 inotify 實例,返回文件描述符,用于內核向用戶態(tài)程序傳輸監(jiān)聽到的 inotify 事件。函數(shù)聲明為:
int inotify_init(void);
內核同時提供了int inotify_init1(int flags),flags 的可選值如下:
IN_NONBLOCK 讀取文件描述符時不會被阻塞,即使沒有數(shù)據(jù)可用也是如此。 如果沒有數(shù)據(jù)可用,則讀操作將立即返回0,而不是等待數(shù)據(jù)可用。IN_CLOEXEC 如果在程序運行時打開了一個文件描述符,并且在調用execve()時沒有將其關閉, 那么在新程序中仍然可以使用該文件描述符。 設置IN_CLOEXEC標志后,可以確保在調用execve()時關閉文件描述符,避免在新程序中使用。
可以通過 OR 指定多個flag,當flags=0等價于int inotify_init(void)。
inotify_add_watch
添加需要監(jiān)聽的目錄或文件(watch list),可以添加新的路徑,也可以是已經(jīng)添加過的路徑。fd 是inotify_init返回的文件描述符,mask 指定需要監(jiān)聽的事件類型,通過 OR 指定多個事件。返回值是當前路徑的wd(watch descriptor),可用于移除對該路徑的監(jiān)聽。
函數(shù)聲明為:
#include <sys/inotify.h>int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
Inotify 支持監(jiān)聽的事件包括:
/* 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
移除被監(jiān)聽的路徑。fd 是inotify_init返回的文件描述符,wd 是inotify_add_watch返回的監(jiān)聽文件描述符。
函數(shù)聲明為:
#include <sys/inotify.h>int inotify_rm_watch(int fd, int wd);實例
以下是基于 Rust 語言實現(xiàn)的實例:
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())?; // 添加被監(jiān)聽的目錄或文件,指定需要監(jiān)聽的事件 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 不會遞歸監(jiān)聽二級目錄下的文件dir1/file2.txt。
經(jīng)測試,Inotify 可以直接監(jiān)聽容器 rootfs 下的目錄:
nerdctl run --rm -it golang./target/debug/inotify /run/containerd/io.containerd.runtime.v2.task/default/CONTAINERD_ID/rootfsFanotify基本介紹
Inotify 能夠監(jiān)聽目錄和文件的事件,但這種 notifiation 機制也存在局限:inotify 只能通知用戶態(tài)進程觸發(fā)了哪些文件系統(tǒng)事件,而無法進行干預,典型的應用場景是殺毒軟件。
Fanotify[4] 的出現(xiàn)就是為了解決這個問題,同時允許遞歸監(jiān)聽目錄下的子目錄和文件。
Fanotify 的工作流程如下:
用戶通過系統(tǒng)調用(如:write、read)操作文件;
內核將文件系統(tǒng)事件發(fā)送到 fsnotify_group 的事件隊列中;
喚醒等待 fanotify 事件的進程(listener);
進程通過 fd 從內核隊列讀取 fanotify 事件;
如果是 FAN_OPEN_PERM 和 FAN_ACCESS_PERM 監(jiān)聽類型,進程需要通過 write 把許可信息(允許 or 拒絕)寫回內核;
內核根據(jù)許可信息決定是否繼續(xù)完成該文件系統(tǒng)事件。
*博客內容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權請聯(lián)系工作人員刪除。