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

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

Linux文件事件監控之Fanotify [一]

監控流程

上文展示了從sys_open()到fsnotify()之間的call trace,接下來繼續追蹤在fsnotify()之後的代碼路徑:

根據ftrace的打印結果,fanotify註冊的"handle_event"函數指針會被調用,進而就是通過fanotify_alloc_event(),給要向listerner上報的內容分配內存,並根據和listener約定的事件格式,填寫相關的字段:

struct fanotify_event *fanotify_alloc_event(struct fsnotify_group *group,
                       struct inode *inode, u32 mask, ...)
{
    struct fanotify_event *event = kmem_cache_alloc(fanotify_event_cachep, gfp);

    fsnotify_init_event(&event->fse, inode);  // event->inode = inode;
    event->mask = mask;
...

這個約定的事件格式主要是由以下這幾部分組成(因爲只包含控制信息,而不包含數據信息,所以被稱爲"metadata"):

struct fanotify_event_metadata {
   __u32 event_len;
   __aligned_u64 mask;
   __s32 fd;
   __s32 pid;
   ...
};

"event_len" 是當前事件的長度(大部分情況下都是定長),"mask" 用於說明發生的是什麼事件(包括open/close/read/write),"fd" 代表被監聽文件的file descriptor,而"pid" 則是操作被監聽文件的進程的編號。自4.20內核引入"FAN_REPORT_TID"這個標誌位後,還可以將上報進程PID的行爲更改爲獲取線程的TID。

if (FAN_GROUP_FLAG(group, FAN_REPORT_TID))
    event->pid = get_pid(task_pid(current));
else
    event->pid = get_pid(task_tgid(current));

大家應該都知道,文件成功open之後,會返回一個文件描述符,可這裏文件的open操作被fanotify“劫持”了,還沒有完成呢,這個"fd"是怎麼來的呢?

此fd實際上是listerner進入內核態後自己創建的(文件描述符這個東西是進程私有的,A進程的fd對B進程來說其實也沒有意義),不過在內核態獲取fd需要「自力更生」,即調用get_unused_fd_flags()找到一個未使用的文件描述符。

int create_fd(struct fsnotify_group *group, struct fanotify_event *event, struct file **file)
{
    int client_fd = get_unused_fd_flags(group->fanotify_data.f_flags);

    if (event->path.dentry && event->path.mnt)
        struct file *new_file = dentry_open(&event->path,
                                group->fanotify_data.f_flags | FMODE_NONOTIFY,
                                current_cred());
    return client_fd;
}

同時,fd是給用戶態的進程用的,在內核裏面,對文件的操作使用的是"struct file",所以對於自行獲取的空閒fd,還需要通過fd_install()來把兩者關聯起來。

struct file *f = NULL;
int fd = create_fd(group, event, &f);
fd_install(fd, f);

【開始等待】

接下來將準備好的上報事件加入notification queue(以下簡稱"nq"),對於需要等待listener裁決的文件事件,操作文件的進程需要阻塞在這裏:

傳統的阻塞等待分爲兩種,其中之一的Interruptible sleep可更快速地響應信號,但是增加了程序編寫的難度。在被喚醒的時候,需要檢測是等待的事件到來還是被信號打斷,如果是被信號打斷,則返回"-EINTR",以便用戶態程序進一步處理(比如繼續睡眠)。

Uninterruptible sleep則消除了這種煩惱,它只會因爲事件的到來而被喚醒。但如果等待的事件由於某種原因一直沒有發生,就將一直等下去,由於不能被任何信號打斷,在這種情況下,你對它可以說是無可奈何。

而這裏fanotify用的是2.6.25內核新增的"TASK_KILLABLE"類型,它在其他方面都和uninterruptible sleep一樣,但允許被fatal signals(即kill信號)打斷。如果陷入異常,可以通過kill信號將其喚醒。

#include <linux/sched.h>
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

【處理事件】

事件已經上報了,接下來就該把舞臺的聚光燈轉回listener進程了。如果沒有在初始化的時候設定"FAN_NONBLOCK",那麼listener將採用阻塞讀取的方式,直到fanotify的"nq"上有數據產生。在讀取的時候,建議使用一個稍大一些的buffer(比如4096字節),這樣一次read()調用可以獲取多個events,有助於提高效率。

經過對被監控文件的分析,listener將作出放行或者阻止的決定,並通過以下的數據格式,回覆給fanotify:

struct fanotify_response {
   __s32 fd;
   __u32 response;
};

【結束等待】

然後,被監控進程就會被喚醒,"nq"上對應的事件entry使命完成,也將被釋放。

finish:
    if (fanotify_is_perm_event(mask))
        fsnotify_finish_user_wait(iter_info);

至此,一輪監控週期就已完成,現在也可以回答上文提出的那個問題,即爲什麼有了epoll還需要fanotify。首先,epoll監聽的是文件的數據是否ready,它不具備監聽文件的open/close事件的能力,此外,epoll也不能對其監聽的文件做阻止訪問的操作。

可靠性和性能

對於fanotify原理和功能實現的介紹告一段落,回顧整個過程,還有兩個問題需要考慮。

  • 一是如果fanotify產生事件的速度過快,listener進程來不及處理,那麼對於容量有限的"nq"來說,就可能造成緩衝區溢出,進而引起事件丟失的後果。"nq"的大小默認爲16384,可通過設置"FAN_UNLIMITED_QUEUE"來解除限制。

但是,如果未處理的事件數量真的過多,解除限制後將加大內存開銷,在陷入異常的情況下,甚至可能使內存耗盡。

另外一個面臨同樣問題的就是配置監聽哪些文件和哪些事件的"marks",它的默認值爲8192,雖然可以更改爲"FAN_UNLIMITED_MARKS"以突破限制,但使用不慎依然面臨內存失衡的風險。

  • 除了可靠性,性能也是其競爭力的重要一環,而性能問題多半離不開cache。如果listener在上一次已經分析過一個文件,那麼當這個文件再次被操作時,fanotify就沒有必要再請示listener了。

在fanotify中,這是通過listener對文件設置ignore mask來實現的。在同一文件系統內,重命名或移動文件不會改變文件的inode編號,因此其對應的cache entry依然有效。但如果文件的內容發生變化,或者被刪除,抑或是移動到了另一文件系統,entry都將會失效。

 

參考:

 

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章