Go 存儲基礎 — “文件” 被偷偷修改?來,給它裝個監控!

堅持思考,就會很酷

背景

我們總有這樣的擔憂:總有刁民想害朕,總有人偷偷在目錄下刪改文件,高危操作想第一時間瞭解,怎麼辦? 而且通常我們還有這樣的需求:

怎麼做到這個事情呢?最常見的通常有三個辦法

  1. 第一種:當事人主動通知你,這是侵入式的,需要當事人修改這部分代碼來支持,依賴於當事人的自覺;

  2. 第二種:輪詢觀察,這個是無侵入式的,你可以自己寫個輪詢程序,每隔一段時間喚醒一次,對文件和目錄做各種判斷,從而得到這個目錄的變化;

  3. 第三種:操作系統支持,以事件的方式通知到訂閱這個事件的用戶,達到及時處理的目的;

很明顯,第三種最好:

  1. 純旁路的邏輯,對線上程序無侵入;

  2. 操作系統直接支持,以事件的形式通知,性能也最好,100% 準確率(比較自己輪詢判斷要好);

怎麼做到這個事情呢?

既然是操作系統的支持,那麼就涉及到系統調用。系統調用直接使用略微複雜了些,Go 裏面有個庫 fsnotify ,就是封裝了系統調用,用來監控文件事件的。當指定目錄或者文件,發生了創建,刪除,修改,重命名的事件,裏面就能得到通知。

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

驚喜就是,這裏能和之前 Linux 編輯器之神 vim 的 IO 存儲原理 篇能結合上:

  1. 看到了 ~ 鏡像文件,還看到了 swp 文件,竟然還看到了 一個 4913 的文件(這個文件也是個臨時文件,感興趣的可以瞭解一下);

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

深層原理

fsnotify 是跨平臺的實現,奇伢這裏只講 Linux 平臺的實現機制。fsnotify 本質上就是對系統能力的一個淺層封裝,主要封裝了操作系統提供的兩個機制:

  1. inotify 機制;

  2. epoll 機制;

旁白:真的是何處都有 epoll 呀。如果還有對 epoll 不明白的趕緊複習下 Linux fd 系列,深度 epoll 剖析

環境聲明

Linux 內核版本 4.19

 1   inotify 機制

什麼是 inotify 機制?

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

劃重點:其實 inotify 機制的誕生源於一個通用的需求,由於 IO / 硬件管理都在內核,但用戶態是有獲悉內核事件的強烈需求,比如磁盤的熱插拔,文件的增刪改。這裏就誕生了三個異曲同工的機制:hotplug 機制、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_accessfsnotify_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 中,尤爲重要,因爲這代表着一切可監控!!!這裏面能做到的事情太多了。

堅持思考,方向比努力更重要。關注我:奇伢雲存儲

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/m0dRfKCeis53V866-vSKGQ