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