使用方法很簡單:
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
- 看到了 ~ 鏡像文件,還看到了 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 機制。划重點:有最明顯的兩個特征:
- inotify fd 的
inotify_fops
實現了.poll
接口; - 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 里面的“數據”讀出來。這個讀出來的所謂的“數據”就是一個個文件事件。
我們看一眼整體的模塊層次:
總結
- Go 的 fsnotify 庫很方便對文件、目錄做監控,這里的充滿了想象力,因為一切皆文件,這代表着一切可監控。童鞋們,這里的想象空間非常大哦;
- 通過 fsnotify 我們映證了 vim 的秘密;
- Go 的 fsnotify 其實操作系統能力的淺層封裝,Linux 本質就是對 inotify 機制;
- inotify 也是一個特殊句柄,屬於匿名句柄之一,這個句柄用於文件的事件監控;
- fsnotify 用 epoll 機制對 inotify fd 的可讀事件進行監控,實現 IO 多路復用的事件通知機制;
后記
“總有刁民想害朕”,終於不怕文件被偷偷動手腳了,有了 fsnotify 之后,文件(目錄)做的任何事情我總能第一時間感知到。
今天又學到一個新的 fd 類型呢,inotify fd,一個用於監控文件事件的機制,這個在一切皆文件的 Linux 中,尤為重要,因為這代表着一切可監控!!!這里面能做到的事情太多了。