go 的 fsnotify 的使用


使用方法很簡單:

  1,先用fsnotify 創建一個監聽器;

  2,然后放到一個單獨的goroutine 監聽事件即可,通過channel的方式傳遞;

  

package main

import (
    "log"
    "github.com/fsnotify/fsnotify"
)

func main() {
    // 創建文件/目錄監聽器
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close()
    done := make(chan bool)
    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    return
                }
                // 打印監聽事件
                log.Println("event:", event)
            case _, ok := <-watcher.Errors:
                if !ok {
                    return
                }
            }
        }
    }()
    // 監聽當前目錄
    err = watcher.Add("./")
    if err != nil {
        log.Fatal(err)
    }
    <-done
}

  

我們測試一下(有驚喜哦)。先把上述程序編譯,然后跑起來:

root@ubuntu:~/code/gopher/src/notify# ./notify 

  

再打開一個終端,准備進行你的操作:

先 touch 一個新文件 hello.txt

 

touch hello.txt

  使用 vim 打開這個文件,寫入一行數據,然后關閉退出:

vim hello.txt

  

root@ubuntu:~/code/gopher/src/notify# ./notify 
# 觸發事件:創建的時候
2021/08/20 17:02:52 event: "./hello.txt": CREATE
2021/08/20 17:02:52 event: "./hello.txt": CHMOD
# 觸發事件:vim 打開初始化的時候(創建 swp 文件)
2021/08/20 17:17:08 event: "./.hello.txt.swp": CREATE
2021/08/20 17:17:08 event: "./.hello.txt.swx": REMOVE
2021/08/20 17:17:08 event: "./.hello.txt.swp": REMOVE
2021/08/20 17:17:08 event: "./.hello.txt.swp": CREATE
2021/08/20 17:17:08 event: "./.hello.txt.swp": WRITE
2021/08/20 17:17:08 event: "./.hello.txt.swp": CHMOD
# 觸發事件::w 寫入保存的時候
2021/08/20 17:17:53 event: "./4913": REMOVE
2021/08/20 17:17:53 event: "./hello.txt": RENAME
2021/08/20 17:17:53 event: "./hello.txt~": CREATE
2021/08/20 17:17:53 event: "./hello.txt": CREATE
2021/08/20 17:17:53 event: "./hello.txt": WRITE
2021/08/20 17:17:53 event: "./hello.txt": CHMOD
2021/08/20 17:17:53 event: "./hello.txt": CHMOD
2021/08/20 17:17:53 event: "./hello.txt~": REMOVE
# 觸發事件::q 的退出時候
2021/08/20 17:17:57 event: "./.hello.txt.swp": WRITE
2021/08/20 17:18:11 event: "./.hello.txt.swp": REMOVE
  1. 看到了 ~ 鏡像文件,還看到了 swp 文件,竟然還看到了 一個 4913 的文件(這個文件也是個臨時文件,感興趣的可以了解一下);

太神奇了,這樣你就有一個新的手段監控你的文件發生的任何事情了。這是什么原理呢?

 

深層原理

fsnotify 本質上就是對系統能力的一個淺層封裝,主要封裝了操作系統提供的兩個機制:

1,inotify 機制

2,epoll 機制

旁白:真的是何處多有epoll呀,如果還有對epoll不明白的趕緊復習下linux fd系列。

環境聲明:

 

1,inotify機制

  什么是inotify機制?

  這是一個內核用於通知用戶空間程序文件系統變化的機制。

  划重點:其實inotify機制的誕生源於一個通用的需求,由於IO/硬件管理都在內核,但用戶是有獲悉內核時間的強烈需求,比如磁盤的熱插拔,文件的增刪改。這里就誕生了三個異曲同工的機制:hoplug 機制,udev管理機制,inotify機制。

 

 

inotify 的三個接口

 

操作系統提供了三個接口來支撐,非常簡潔:

// fs/notify/inotify/inotify_user.c

// 創建 notify fd
inotify_init1

// 添加監控路徑
inotify_add_watch

// 刪除一個監控
inotify_rm_watch

  

用法非常簡單,分別對應 inotify fd 的創建,監控的添加和刪除。

 

inotify 怎么實現監控的?

 

inotify 支持監聽的事件非常多,除了增刪改,還有訪問,移動,打開,關閉,設備卸載等等事件。

內核要上報這些文件 api 事件必然要采集這些事件。在哪一個內核層次采集的呢?

 

統調用 -> vfs -> 具體文件系統( ext4 )-> 塊層 -> scsi 層

**答案是:vfs 層。**其實這個很容易理解,這是必然的,因為這是所有“文件”操作的入口。

以 vfs 的 read/write 為例,我們看一下:

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    // ...
    ret = __vfs_read(file, buf, count, pos);
    if (ret > 0) {
        // 事件采集點:訪問事件
        fsnotify_access(file);
    }

}

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
    // ...
    ret = __vfs_write(file, buf, count, pos);
    if (ret > 0) {
        // 事件采集點:修改事件
        fsnotify_modify(file);
    }
}

fsnotify_access 和 fsnotify_modify 就是 inotify 機制的一員。有一系列 fsnotify_xxx 的函數,定義在 include/linux/fsnotify.h ,這函數里面全都調用到 fsnotify 這個函數。、

 

static inline void fsnotify_modify(struct file *file)
{
    // 獲取到 inode

    if (!(file->f_mode & FMODE_NONOTIFY)) {
        fsnotify_parent(path, NULL, mask);
        // 采集事件,通知到指定結構
        fsnotify(inode, mask, path, FSNOTIFY_EVENT_PATH, NULL, 0);
    }
}

來看一下 fsnotify 的函數實現,我們簡單的梳一下調用棧:

fsnotify
    -> send_to_group
        -> inotify_handle_event
            -> fsnotify_add_event
                -> wake_up (喚醒等待隊列,也就是 epoll)

再看一眼具體的實現(其實非常簡單,就是一個事件通知):

// 把事件通知到相應的 group 上;
int fsnotify(struct inode *to_tell, __u32 mask, const void *data, int data_is, const unsigned char *file_name, u32 cookie)
{
        // ...
        // 把事件通知給正在監聽的 fsnotify_group
        while (fsnotify_iter_select_report_types(&iter_info)) {
                ret = send_to_group(to_tell, mask, data, data_is, cookie, file_name, &iter_info);
                if (ret && (mask & ALL_FSNOTIFY_PERM_EVENTS))
                        goto out;
                fsnotify_iter_next(&iter_info);
        }
out:
        return ret;
}

static int send_to_group(struct inode *to_tell, __u32 mask, const void *data, int data_is, u32 cookie, const unsigned char *file_name, struct fsnotify_iter_info *iter_info)
{
    // 通知相應的 group ,有事來了!
    return group->ops->handle_event(group, to_tell, mask, data, data_is, file_name, cookie, iter_info);
}

// group->ops->handle_event 被賦值為 inotify_handle_event

int inotify_handle_event(struct fsnotify_group *group, struct inode *inode, u32 mask, const void *data, int data_type, const unsigned char *file_name, u32 cookie, struct fsnotify_iter_info *iter_info)
{
    // 喚醒事件,通知相應的 group
    ret = fsnotify_add_event(group, fsn_event, inotify_merge);
}


// 添加事件到 group 
int fsnotify_add_event(struct fsnotify_group *group, struct fsnotify_event *event, int (*merge)(struct list_head *, struct fsnotify_event *))
{
    // 喚醒這個等待隊列
    wake_up(&group->notification_waitq);
}

 

這里面的邏輯非常簡單:把這次的事件通知給關注的 fsnotify_group 結構體,換句話說,就是把事件通知給 inotify fd。

這個就有意思了,inotify fd 句柄創建的時候,file->private_data 上就綁定了一個 fsnotify_group ,這就對上了。這樣的話,針對文件的所有操作,都能有一份事件發送到 fsnotify_group 上,inotify fd 就有可讀事件了。

 

inotify 也有支持 epoll 機制

在前面我們也提到了,Go 的 fsnotify 主要使用了兩個系統機制 inotify 機制和 epoll 機制。fsnotify 把 inotify fd 放到 epoll 池里面管理。

換句話說,inotify fd 支持 epoll 機制划重點:有最明顯的兩個特征

  1. inotify fd 的 inotify_fops 實現了 .poll 接口;
  2. inotify fd 相關的某個結構體一定有個 wait 隊列的表頭

這個結構體是啥?

其實跟 timerfd 類似(讀者有不熟悉的,可以去復習下哦),筆者直接揭秘啦,這個結構體就是 fsnotify_group 。被存放在 inotify fd 對應的 file->private_data 字段。這個 wait 隊列表頭就是 group->notification_waitq 。

來看一眼結構體的簡要關系:

 

  2   epoll 機制

 

回到 Go 的 fsnotify 庫的實現原理,fsnotify 利用的第二個系統機制就是 epoll 。inotify fd 通過 inotify_init1 創建出來之后,會把 inotify fd 注冊進 epoll 管理,監聽 inotify fd 的可讀事件。

inotify fd 的可讀事件能是啥?

就是它監聽的文件或者路徑發生的增刪改的事件嘛,這些事件就是內核 inotify 報上來的。

報上來之后,epoll 監控到 inotify fd 可讀,用戶通過 read 調用,把 inotify fd 里面的“數據”讀出來。這個讀出來的所謂的“數據”就是一個個文件事件。

我們看一眼整體的模塊層次:

 

 

總結

 

  1. Go 的 fsnotify 庫很方便對文件、目錄做監控,這里的充滿了想象力,因為一切皆文件,這代表着一切可監控。童鞋們,這里的想象空間非常大哦;
  2. 通過 fsnotify 我們映證了 vim 的秘密;
  3. Go 的 fsnotify 其實操作系統能力的淺層封裝,Linux 本質就是對 inotify 機制;
  4. inotify 也是一個特殊句柄,屬於匿名句柄之一,這個句柄用於文件的事件監控
  5. fsnotify 用 epoll 機制對 inotify fd 的可讀事件進行監控,實現 IO 多路復用的事件通知機制;


圖片

后記

圖片

 

“總有刁民想害朕”,終於不怕文件被偷偷動手腳了,有了 fsnotify 之后,文件(目錄)做的任何事情我總能第一時間感知到。

今天又學到一個新的 fd 類型呢,inotify fd,一個用於監控文件事件的機制,這個在一切皆文件的 Linux 中,尤為重要,因為這代表着一切可監控!!!這里面能做到的事情太多了。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM