Linux文件事件監控之Fanotify [一]【轉】

轉自:https://zhuanlan.zhihu.com/p/186027813

從監聽到監控

Linux的文件事件監聽的原理並不複雜,簡單說就是當一個應用層的進程操作一個目錄或文件時,會觸發system call,此時內核的notification子系統可以守在那裏,把該進程對文件的操作上報給應用層的監聽進程(稱爲listerner)。

在這個領域,自2001年的2.4版本就引入的dnotify可以說是先驅,然而,它的缺陷實在太多了。正如其名字傳達的,只能監控directory,並且它採用的是signal機制來向listener發送通知,因而可以攜帶的信息很有限。

所以,dnotify基本已經成了「先烈」,一個更優秀的名爲"inotify"的監聽機制於2005年在2.6.13內核中亮相,它除了可以監控目錄,還可以監聽普通文件產生的事件。此外,inotify擯棄了signal機制,改爲通過event queue的形式向listener上傳事件的相關信息。

看起來,inotify已經算是比較完備了,但是它在應用上還是存在諸多限制。其中一個最明顯的問題是它只能"notify",作爲listener,只是知道有這個文件事件發生了,最多記錄一下,但並不能改變什麼(比如被殺毒軟件判定爲病毒文件,就需要阻止這個操作)。

到了2.6.36內核,fanotify的出現解決了這個痛點,允許listener介入並改變文件事件的行爲,纔算實現了從“監聽”到“監控”的跨越,同時也擴展了其應用的範圍。

兩個API

fanotify的調用接口是比較簡潔的,主要有2個API,但其可攜帶的參數還是爲它的使用增加了很多的靈活性。作爲listerner,首先需要通過fanotify_init()函數來設定監控的模式:

#include <fcntl.h>
#include <sys/fanotify.h>

int fanotify_init(unsigned int flags, unsigned int event_f_flags);

如果"flags"爲"FAN_CLASS_CONTENT",意思是當一個文件的content準備好後,listerner就要介入了,然後給出阻止/放行的決定。爲"FAN_CLASS_NOTIF"就是listerner只需要被notify就可以了,類似於「已閱」,但不給出任何的修改意見。

雖然fanotify在內核中並不是作爲一個module存在的,也沒有對應的設備文件,但從抽象的角度,你完全可以把它視作一個特殊的文件,而"fanotify_init"就是去"open"這個文件,因此這裏的"event_f_flags" 其實和VFS中open()系統調用的"flags" 參數差不多(比如" O_RDONLY"),這也是爲什麼需要包含"fcntl.h"頭文件的原因。

同open()一樣,該函數返回的也是代表fanotify這個特殊文件的file descriptor,獲得了fd後,就可以利用fanotify_mark()來展開具體的配置了:

int fanotify_mark(int fanotify_fd, unsigned int flags,
                  uint64_t mask, int dirfd, const char *pathname);

文件事件的監控大致包括兩個維度:對哪些文件產生的事件感興趣,以及對這些文件的哪些事件感興趣。"flags" 就是用來增加/刪除感興趣的文件列表(通過"FAN_MARK_ADD"/"FAN_MARK_REMOVE"等),而"mask" 就是用來設置感興趣的事件列表,比如"FAN_OPEN"就是當文件被打開時,產生一個事件給listener。

至於"dirfd" "pathname",則是用來給出文件路徑的,比如"AT_FDCWD"就表示從當前路徑(Current Working Directory)開始解析。

一個mark針對一個文件,那它是怎麼和內核中表示文件的數據結構關聯起來的呢?文件的唯一標識是inode,mark通過一個"fsnotify_mark_connector"類型的指針,指向了inode結構體中對應的元素(至於這裏爲什麼是叫"fsnotify"而不是"fanotify",將在本文後半部分給出答案):

熟悉epoll的同學可能已經發現,fanotify_init()其實和epoll_create()挺像的,都是用來監聽文件,而本身的instance也作爲一個文件,有對應的file descriptor供應用程序操作。fanotify_mark()則可類比於epoll_ctl(),都是用來增刪和修改事件列表的。

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

不過,fanotify並沒有epoll_wait()這樣的輪詢機制,不能直接讓listerner收到事件,所以在具體的使用上,fanotify和poll/epoll是層級的關係:

那爲什麼不直接只用epoll呢?這還得從fanotify的原理說起。先來看下對於一個被監控文件進行open操作時,fanotify是怎麼發揮作用的:

在do_dentry_open()之前,都是VFS的標準調用路徑,而到了security_file_open()這裏,已經準備好hook,在此潛伏已久的各路人馬就開始出手了:

int security_file_open(struct file *file)
{
    int ret = call_int_hook(file_open, 0, file);
    if (ret)
        return ret;
		
    return fsnotify_perm(file, MAY_OPEN);
}

這些人馬主要來自LSM(Linux Security Module),比如著名的SELinux(參考這篇文章):

call_int_hook -->
hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { 
    RC = P->hook.FUNC(__VA_ARGS__);		
    if (RC != 0)				
        break;
}

統一入口

走到fsnotify_perm()這裏,終於輪到前面已經露過一回臉的fsnotify了。從dnotify到inoitfy,再到fanotify,缺陷和限制逐漸減少,但這不是一個迭代的關係,它們仨的代碼目前都存在於"/fs/notify"目錄。

同時,由於機制的相似性,它們存在很多共通之處,將這些共通之處提取出來,就成了fsnotify。fsnotify作爲後端,負責接收文件事件,它被作爲前端、和listener直接交互的dnotify, inotify和fanotify所共享。

每一個前端instance被抽象爲一個"group"(在代碼中由"fsnotify_group"結構體表示),每個group都有自己的notification queue(以下簡稱"nq"),用於向listener傳遞事件。

從效率的角度,fsnotify不會把收到的事件依次放到每個group的"nq"上,而是隻維護一個event queue,根據各個group配置的mask,在其對應的"nq"裏存放指針,指向event queue中感興趣的事件。

那fsnotify具體是如何將事件送到fanotify這個group,fanotify又是怎樣和應用層的listener打交道的呢,請看下文分解。

 

參考:

原創文章,轉載請註明出處。

 

編輯於 2020-10-23 11:35
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章