監(jiān)聽風(fēng)云 | Inotify 實(shí)現(xiàn)原理
本文轉(zhuǎn)載自微信公眾號「Linux內(nèi)核那些事」,作者songsong001。轉(zhuǎn)載本文請聯(lián)系Linux內(nèi)核那些事公眾號。
重要的數(shù)據(jù)結(jié)構(gòu)
魯迅先生說過:程序 = 數(shù)據(jù)結(jié)構(gòu) + 算法
想想如果讓我們來設(shè)計(jì) inotify 應(yīng)該如何實(shí)現(xiàn)呢?下面來分析一下:
- 我們知道,inotify 是用來監(jiān)控文件或目錄的變動事件,所以應(yīng)該定義一個對象來存儲被監(jiān)聽的文件或目錄列表和它們所發(fā)生的事件列表(在內(nèi)核中定義了 inotify_device 對象來存儲被監(jiān)聽的文件列表和事件列表)。
- 另外,當(dāng)對被監(jiān)聽的文件或目錄進(jìn)行讀寫操作時會觸發(fā)相應(yīng)的事件產(chǎn)生。所以,應(yīng)該在讀寫操作相關(guān)的系統(tǒng)調(diào)用中嵌入產(chǎn)生事件的動作(在內(nèi)核中由 inotify_dev_queue_event 函數(shù)產(chǎn)生事件)。
在介紹 inotify 的實(shí)現(xiàn)前,我們先來了解下其原理。inotify 的原理如下:
當(dāng)用戶調(diào)用 read 或者 write 等系統(tǒng)調(diào)用對文件進(jìn)行讀寫操作時,內(nèi)核會把事件保存到 inotify_device 對象的事件隊(duì)列中,然后喚醒等待 inotify 事件的進(jìn)程。正所謂一圖勝千言,所以我們通過下圖來描述此過程:
從上圖可知,當(dāng)應(yīng)用程序調(diào)用 read 函數(shù)讀取文件的內(nèi)容時,最終會調(diào)用 inotify_dev_queue_event 函數(shù)來觸發(fā)事件,調(diào)用棧如下:
- read()
- └→ sys_read()
- └→ vfs_read()
- └→ fsnotify_access()
- └→ inotify_inode_queue_event()
- └→ inotify_dev_queue_event()
inotify_dev_queue_event 函數(shù)主要完成兩個工作:
- 創(chuàng)建一個表示事件的 inotify_kernel_event 對象,并且把其插入到 inotify_device 對象的 events 列表中。
- 喚醒正在等待 inotify 發(fā)生事件的進(jìn)程,等待的進(jìn)程放置在 inotify_device 對象的 wq 字段中。
上面主要涉及到兩個對象,inotify_device 和 inotify_kernel_event,我們先來介紹一下這兩個對象的作用。
- inotify_device:內(nèi)核使用此對象來描述一個 inotify,是 inotify 的核心對象。
- intoify_kernel_event:內(nèi)核使用此對象來描述一個事件。
我們來看看這兩個對象的定義。
1. inotify_device對象
內(nèi)核使用 inotify_device 來管理 inotify 監(jiān)聽的對象和發(fā)生的事件,其定義如下:
- 1struct inotify_device {
- 2 wait_queue_head_t wq;
- 3 ...
- 4 struct list_head events;
- 5 ...
- 6 struct inotify_handle *ih;
- 7 unsigned int event_count;
- 8 unsigned int max_events;
- 9};
下面我們介紹一下各個字段的作用:
- wq:正在等待當(dāng)前 inotify 發(fā)生事件的進(jìn)程列表。
- events:保存由 inotify 監(jiān)聽的文件或目錄所發(fā)生的事件。
- ih:內(nèi)核用來存儲 inotify 監(jiān)聽的文件或目錄,下面會介紹。
- event_count:inotify 監(jiān)聽的文件或目錄所發(fā)生的事件數(shù)量。
- max_events:inotify 能夠保存最大的事件數(shù)量。
下圖描述了 inotify_device 對象中兩個比較重要的隊(duì)列(等待隊(duì)列 和 事件隊(duì)列):
當(dāng)事件隊(duì)列中有數(shù)據(jù)時,就可以通過調(diào)用 read 函數(shù)來讀取這些事件。
2. inotify_kernel_event對象
內(nèi)核使用 inotify_kernel_event 對象來存儲一個事件,其定義如下:
- struct inotify_kernel_event {
- struct inotify_event event;
- struct list_head list;
- char *name;
- };
可以看出,inotify_kernel_event 對象只是對 inotify_event 對象進(jìn)行擴(kuò)展而已,而我們在《監(jiān)聽風(fēng)云 - inotify介紹》一文中已經(jīng)介紹過 inotify_event 對象。
inotify_kernel_event 對象在 inotify_event 對象的基礎(chǔ)上增加了 list 字段和 name 字段:
- list:用于把所有由 inotify 監(jiān)聽的文件或目錄所發(fā)生的事件連接起來,
- name:用于記錄發(fā)生事件的文件名或目錄名。
3. inotify_handle對象
在 inotify_device 對象中,有個類型為 inotify_handle 的字段 ih,這個字段主要用來存儲 inotify 監(jiān)聽的文件或目錄。我們來看看 inotify_handle 對象的定義:
- struct inotify_handle {
- struct idr idr;
- ...
- struct list_head watches;
- ...
- const struct inotify_operations *in_ops;
- };
下面來介紹一下 inotify_handle 對象的各個字段作用:
- idr:ID生成器,用于生成被監(jiān)聽對象(文件或目錄)的ID。
- watches:inotify 監(jiān)聽的對象(文件或目錄)列表。
- in_ops:當(dāng)事件發(fā)生時,被 inotify 回調(diào)的函數(shù)列表。
4. inotify_watch對象
內(nèi)核使用 inotify_handle 來存儲被監(jiān)聽的對象列表,那么被監(jiān)聽對象是個什么東西呢?內(nèi)核中使用 inotify_watch 對象來表示一個被監(jiān)聽的對象。其定義如下:
- struct inotify_watch {
- struct list_head h_list;
- struct list_head i_list;
- ...
- struct inotify_handle *ih;
- struct inode *inode;
- __s32 wd;
- __u32 mask;
- };
下面介紹一下 inotify_watch 對象各個字段的作用:
- h_list:用于把屬于同一個 inotify 監(jiān)聽的對象連接起來。
- i_list:由于同一個文件或目錄可以被多個 inotify 監(jiān)聽,所以使用此字段來把所有監(jiān)聽同一個文件的 inotify_handle 對象連接起來。
- ih:指向其所屬的 inotify_handle 對象。
- inode:由于在 Linux 內(nèi)核中,每個文件或目錄都由一個 inode 對象來描述,這個字段就是指向被監(jiān)聽的文件或目錄的 inode 對象。
- wd:被監(jiān)聽對象的ID(或稱為描述符)。
- mask:被監(jiān)聽的事件類型(在《監(jiān)聽風(fēng)云 - inotify介紹》一文中已經(jīng)介紹)。
現(xiàn)在,我們通過下圖來描述一下 inotify_device、inotify_handle 和 inotify_watch 三者的關(guān)系:
inotify功能實(shí)現(xiàn)
上面我們把 inotify 功能涉及的所有數(shù)據(jù)結(jié)構(gòu)都介紹了,有上面的基礎(chǔ),現(xiàn)在我們可以開始分析 inotify 功能的實(shí)現(xiàn)了。
1. inotify_init 函數(shù)
在《監(jiān)聽風(fēng)云 - inotify介紹》一文中介紹過,要使用 inotify 功能,首先要調(diào)用 inotify_init 函數(shù)創(chuàng)建一個 inotify 的句柄,而 inotify_init 函數(shù)最終會調(diào)用內(nèi)核函數(shù) sys_inotify_init。我們來分析一下 sys_inotify_init 的實(shí)現(xiàn):
- long sys_inotify_init(void)
- {
- struct inotify_device *dev;
- struct inotify_handle *ih;
- struct user_struct *user;
- struct file *filp;
- int fd, ret;
- // 1. 獲取一個沒用被占用的文件描述符
- fd = get_unused_fd();
- ...
- // 2. 獲取一個文件對象
- filp = get_empty_filp();
- ...
- // 3. 創(chuàng)建一個 inotify_device 對象
- dev = kmalloc(sizeof(struct inotify_device), GFP_KERNEL);
- ...
- // 4. 創(chuàng)建一個 inotify_handle 對象
- ih = inotify_init(&inotify_user_ops);
- ...
- // 5. 把 inotify_handle 對象與 inotify_device 對象進(jìn)行綁定
- dev->ih = ih;
- // 6. 設(shè)置文件對象的操作函數(shù)列表為:inotify_fops
- filp->f_op = &inotify_fops;
- ...
- // 7. 將 inotify_device 對象綁定到文件對象的 private_data 字段中
- filp->private_data = dev;
- ...
- // 8. 把文件句柄與文件對象進(jìn)行映射
- fd_install(fd, filp);
- return fd;
- }
sys_inotify_init 函數(shù)主要完成以下幾個工作:
- 調(diào)用 get_unused_fd 函數(shù)從進(jìn)程中獲取一個沒被使用的文件描述符(句柄)。
- 調(diào)用 get_empty_filp 獲取一個文件對象。
- 調(diào)用 kmalloc 函數(shù)申請一個 inotify_device 對象。
- 調(diào)用 inotify_init 函數(shù)創(chuàng)建并初始化一個 inotify_handle 對象。
- 把 inotify_handle 對象與 inotify_device 對象進(jìn)行綁定。
- 設(shè)置文件對象的操作函數(shù)列表為:inotify_fops,主要提供 read 和 poll 等接口的實(shí)現(xiàn)。
- 將 inotify_device 對象綁定到文件對象的 private_data 字段中。
- 把文件描述符與文件對象進(jìn)行映射。
- 返回文件描述符給應(yīng)用層。
從上面的實(shí)現(xiàn)可以看出,sys_inotify_init 函數(shù)主要是創(chuàng)建 inotify_device 對象和 inotify_handle 對象,并且將它們與文件對象關(guān)聯(lián)起來。
另外需要注意的是,在 sys_inotify_init 函數(shù)中,還把文件對象的操作函數(shù)集設(shè)置為 inotify_fops,主要提供了 read 和 poll 等接口的實(shí)現(xiàn),其定義如下:
- static const struct file_operations inotify_fops = {
- .poll = inotify_poll,
- .read = inotify_read,
- .release = inotify_release,
- ...
- };
所以,當(dāng)調(diào)用 read 函數(shù)讀取 inotify 的句柄時,就會觸發(fā)調(diào)用 inotify_read 函數(shù)讀取 inotify 事件隊(duì)列中的事件。
2. inotify_add_watch 函數(shù)
當(dāng)調(diào)用 inotify_init 函數(shù)創(chuàng)建好 inotify 句柄后,就可以通過調(diào)用 inotify_add_watch 函數(shù)向 inotify 句柄添加要監(jiān)控的文件或目錄。inotify_add_watch 函數(shù)的實(shí)現(xiàn)如下:
- long sys_inotify_add_watch(int fd, const char __user *path, u32 mask)
- {
- struct inode *inode;
- struct inotify_device *dev;
- struct nameidata nd;
- struct file *filp;
- int ret, fput_needed;
- unsigned flags = 0;
- // 通過文件句柄獲取文件對象
- filp = fget_light(fd, &fput_needed);
- ...
- // 獲取文件或目錄對應(yīng)的 inode 對象
- ret = find_inode(path, &nd, flags);
- ...
- inode = nd.dentry->d_inode;
- // 從文件對象的 private_data 字段獲取對應(yīng)的 inotify_device 對象
- dev = filp->private_data;
- ...
- // 創(chuàng)建一個新的 inotify_watch 對象
- if (ret == -ENOENT)
- ret = create_watch(dev, inode, mask);
- ...
- return ret;
- }
sys_inotify_add_watch 函數(shù)主要完成以下幾個工作:
- 調(diào)用 fget_light 函數(shù)獲取 inotify 句柄對應(yīng)的文件對象。
- 調(diào)用 find_inode 函數(shù)獲取 path 路徑對應(yīng)的 inode 對象,也就是獲取要監(jiān)聽的文件或目錄所對應(yīng)的 inode 對象。
- 從 inotify 文件對象的 private_data 字段中,獲取對應(yīng)的 inotify_device 對象。
- 調(diào)用 create_watch 函數(shù)創(chuàng)建一個新的 inotify_watch 對象,并且把這個 inotify_watch 對象添加到 inotify_handle 對象的 watches 列表和 inode 對象的 inotify_watches 列表中。
事件通知
到了 inotify 最關(guān)鍵的部分,就是 inotify 的事件是怎么產(chǎn)生的。
在本文的第一部分中介紹過,當(dāng)用戶調(diào)用 read 系統(tǒng)調(diào)用讀取文件內(nèi)容時,最終會調(diào)用 inotify_dev_queue_event 函數(shù)來產(chǎn)生一個事件,我們先來回顧一下 read 系統(tǒng)調(diào)用的調(diào)用棧:
- read()
- └→ sys_read()
- └→ vfs_read()
- └→ fsnotify_access()
- └→ inotify_inode_queue_event()
- └→ inotify_dev_queue_event()
下面我們來分析一下 inotify_dev_queue_event 函數(shù)的實(shí)現(xiàn):
- static void
- inotify_dev_queue_event(struct inotify_watch *w, u32 wd,
- u32 mask, u32 cookie, const char *name, struct inode *ignored)
- {
- struct inotify_user_watch *watch;
- struct inotify_device *dev;
- struct inotify_kernel_event *kevent, *last;
- watch = container_of(w, struct inotify_user_watch, wdata);
- dev = watch->dev;
- ...
- // 1. 申請一個 inotify_kernel_event 事件對象
- if (unlikely(dev->event_count == dev->max_events))
- kevent = kernel_event(-1, IN_Q_OVERFLOW, cookie, NULL);
- else
- kevent = kernel_event(wd, mask, cookie, name);
- ...
- // 2. 增加 inotify 事件隊(duì)列的計(jì)數(shù)器
- dev->event_count++;
- // 3. 增加 inotify 事件隊(duì)列所占用的內(nèi)存大小
- dev->queue_size += sizeof(struct inotify_event) + kevent->event.len;
- // 4. 把事件對象添加到 inotify 的事件隊(duì)列中
- list_add_tail(&kevent->list, &dev->events);
- // 5. 喚醒正在等待讀取事件的進(jìn)程
- wake_up_interruptible(&dev->wq);
- ...
我們先來介紹一下 inotify_dev_queue_event 函數(shù)各個參數(shù)的意義:
- w:被監(jiān)聽對象,用于描述被監(jiān)聽的文件或目錄。
- wd:被監(jiān)聽對象的ID。
- mask:發(fā)生的事件類型,可以參考《監(jiān)聽風(fēng)云 - inotify介紹》一文。
- cookie:比較少使用,忽略。
- name:發(fā)生事件的文件或目錄名稱。
- ignored:發(fā)生事件的文件或目錄的 inode 對象,在本函數(shù)中沒有使用。
inotify_dev_queue_event 函數(shù)主要完成以下幾個工作:
- 通過調(diào)用 kernel_event 函數(shù)申請一個 inotify_kernel_event 事件對象。
- 增加 inotify 事件隊(duì)列的計(jì)數(shù)器。
- 增加 inotify 事件隊(duì)列所占用的內(nèi)存大小。
- 把第一步創(chuàng)建的事件對象添加到 inotify 的事件隊(duì)列中。
- 喚醒正在等待讀取事件的進(jìn)程(因?yàn)橐呀?jīng)有事件發(fā)生了)。
從上面的分析可以看出,inotify_dev_queue_event 函數(shù)只負(fù)責(zé)創(chuàng)建一個事件對象,并且添加到 inotify 的事件隊(duì)列中。但發(fā)生了什么事件是由哪個步驟指定的呢?
我們可以通過分析 read 系統(tǒng)調(diào)用的調(diào)用棧,會發(fā)現(xiàn)在 fsnotify_access 函數(shù)中指定了事件的類型,我們來看看 fsnotify_access 函數(shù)的實(shí)現(xiàn):
- static inline void fsnotify_access(struct dentry *dentry)
- {
- struct inode *inode = dentry->d_inode;
- u32 mask = IN_ACCESS; // 指定事件類型為 IN_ACCESS
- if (S_ISDIR(inode->i_mode))
- mask |= IN_ISDIR; // 如果是目錄, 增加 IN_ISDIR 標(biāo)志
- ...
- // 創(chuàng)建事件
- inotify_inode_queue_event(inode, mask, 0, NULL, NULL);
- }
從上面的分析可知,當(dāng)發(fā)生讀事件時,由 fsnotify_access 函數(shù)指定事件類型為 IN_ACCESS。在 include/linux/fsnotify.h 文件中還實(shí)現(xiàn)了其他事件的觸發(fā)函數(shù),有興趣的可以自行查閱此文件 。
總結(jié)
inotify 的實(shí)現(xiàn)過程總結(jié)為以下兩點(diǎn):
當(dāng)用戶調(diào)用讀寫、創(chuàng)建或刪除文件的系統(tǒng)調(diào)用時,內(nèi)核會注入相應(yīng)的事件觸發(fā)函數(shù)來產(chǎn)生一個事件,并且添加到 inotify 的事件隊(duì)列中。
喚醒等待讀取事件的進(jìn)程,當(dāng)進(jìn)程被喚醒后,就可以通過調(diào)用 read 函數(shù)來讀取 inotify 事件隊(duì)列中的事件。