博客專欄

EEPW首頁 > 博客 > 監(jiān)聽容器中的文件系統(tǒng)事件

監(jiān)聽容器中的文件系統(tǒng)事件

發(fā)布人:電子禪石 時間:2024-09-18 來源:工程師 發(fā)布文章
基本概念

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 的工作流程如下:

  1. 用戶通過系統(tǒng)調用(如:write、read)操作文件;

  2. 內核將文件系統(tǒng)事件保存到 fsnotify_group 的事件隊列中;

  3. 喚醒等待 inotify 的進程(listener);

  4. 進程通過 fd 從內核隊列讀取 inotify 事件。

圖片

其中,inotify_event_info 的定義如下:

c

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ù)聲明為:

c

內核同時提供了int inotify_init1(int flags),flags 的可選值如下:

c

可以通過 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ù)聲明為:

c

Inotify 支持監(jiān)聽的事件包括:

c
  • inotify_rm_watch

移除被監(jiān)聽的路徑。fd 是inotify_init返回的文件描述符,wd 是inotify_add_watch返回的監(jiān)聽文件描述符。

函數(shù)聲明為:

c
實例

以下是基于 Rust 語言實現(xiàn)的實例:

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())?;    // 添加被監(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(())}

編譯&測試:

shell
cargo build./target/debug/inotify test

圖片

可以看到,inotify 不會遞歸監(jiān)聽二級目錄下的文件dir1/file2.txt。

經(jīng)測試,Inotify 可以直接監(jiān)聽容器 rootfs 下的目錄:

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

圖片

Fanotify基本介紹

Inotify 能夠監(jiān)聽目錄和文件的事件,但這種 notifiation 機制也存在局限:inotify 只能通知用戶態(tài)進程觸發(fā)了哪些文件系統(tǒng)事件,而無法進行干預,典型的應用場景是殺毒軟件。

Fanotify[4] 的出現(xiàn)就是為了解決這個問題,同時允許遞歸監(jiān)聽目錄下的子目錄和文件。

Fanotify 的工作流程如下:

  1. 用戶通過系統(tǒng)調用(如:write、read)操作文件;

  2. 內核將文件系統(tǒng)事件發(fā)送到 fsnotify_group 的事件隊列中;

  3. 喚醒等待 fanotify 事件的進程(listener);

  4. 進程通過 fd 從內核隊列讀取 fanotify 事件;

  5. 如果是 FAN_OPEN_PERM 和 FAN_ACCESS_PERM 監(jiān)聽類型,進程需要通過 write 把許可信息(允許 or 拒絕)寫回內核;

  6. 內核根據(jù)許可信息決定是否繼續(xù)完成該文件系統(tǒng)事件。

監(jiān)監(jiān)聽容器中的文件系統(tǒng)事件 - abin在路上 - 博客園 (cnblogs.com)

*博客內容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權請聯(lián)系工作人員刪除。



關鍵詞: fanotify

技術專區(qū)

關閉