Inotify 是檔案系統事件監控機制,計劃包含在即將發布的 Linux 核心中作為 dnotify 的有效替代。dnotify 是較早核心支援的檔案監控機制。Inotify一種強大的、細粒度的、非同步機制,它滿足各種各樣的檔案監控需要,不僅限於安全和效能。下面讓我們一起學習如何安裝 inotify 和如何構建一個樣本使用者空間應用程式來回應檔系統事件。
檔案系統事件監控對於從檔案管理工具到安全工具的各種程式都是必要的,但是 dnotify(早期核心中的標準)存在一些局限性,這使我們期待出現一種更加完善的機制。抱著這種期待,我們發現了 inotify,一種更加現代化的檔案系統事件監控替代品。
為什麼使用 inotify?
使用 inotify 取代 dnotify 的原因有很多。第一個原因是,dnotify 需要您為每個打算監控是否發生改變的目錄開啟一個檔案描述符。當同時監控多個目錄時,這會消耗大量的資源,因為有可能達到每個進程的檔案描述符限制。
除此之外,檔案描述符會鎖定目錄,不允許卸載(unmount)支援的裝置,這在存在可移動介質的環境中會引發問題。在使用 inotify 時,如果正在監控被卸載的檔案系統上的檔案,那麼監控會被自動移除並且您會接收到一個卸載事件。
dnotify 不如 inotify 的第二個原因是 dnotify 有點複雜。注意,使用 dnotify 基礎設施的簡單檔案系統監控粒度只停留於目錄層級。為了使用 dnotify 進行更細粒度的監控,應用程式編程人員必須為每個受監控的目錄保留一個 stat
結構的緩衝。該使用者空間的 stat
結構緩衝需要用來明確確定當接收到通知訊號時目錄發生了什麼變化。當獲得通知訊號時,產生 stat
結構列表並與最新的狀態相比較。顯而易見,這種技術是不理想的。
inotify 的另一個優點是它使用檔案描述符作為基本介面,使應用程式開發人員使用 select
和 poll
來監控裝置。這允許有效多路 I/O 和與 Glib 的 mainloop
的整合。相反,dnotify 所使用的訊號常常使程式員頭疼並且感覺不太優雅。
inotify 通過提供一個更優雅的 API 解決了這些問題,該 API 使用最少的檔案描述符,並確保更細粒度的監控。與 inotify 的通訊是通過裝置節點提供的。基於以上原因,對於監控 Linux 2.6 平台上的檔案,inotify 是您最明智的選擇。
安裝 inotify
安裝 inotify 的第一步是確定您使用的 Linux 核心是否支援它。檢查發行版的最簡單方法是,尋找是否存在 /dev/inotify 裝置。如果存在該裝置,您可以跳到 在簡單應用程式中使用 inotify 一節。
在撰寫本文時,inotify 包含在 Andrew Morton 的 Linux 2.6-mm 分類樹中,而且一些 Linux 發行版正在提供支援 inotify 的核心(包括 Gentoo 和 Ubuntu)或者具有提供支援的補充核心包(例如 Fedora 和 SuSE)。因為 Andrew 可能會根據需要從分類樹刪除對 inotify 的支援,並且 inotify 版本還處於頻繁的開發階段,所以強烈建議您從頭開始打補丁。
如果缺少該裝置,您可能需要對核心打補丁並建立該裝置。
為 inotify 對核心打補丁
可以從 Linux Kernel Archives 獲得 inotify 補丁(請參閱 參考資料 一節的連結)。
您應該為特定的核心應用最高版本編號的補丁。每個發行版處理核心的安裝都有所不同,但以下介紹的是一個通用指導。注意:從 Linux Kernel Archives 擷取發行版 2.6 Linux 核心源檔案,如果合適,請擷取最新的穩定版本。
從進入核心源檔案目錄開始:
bash:~$ cd /usr/src
因為您早先安裝了核心源檔案,現在需要將它解壓縮:
bash:~$ sudo tar jxvf linux-source-2.6.8.1.tar.bz2
現在,使您的 symlink 指向新的源檔案分類樹:
bash:~$ sudo ln -sf linux-source-2.6.8.1 linux
改變目前的目錄到剛才建立的核心源檔案目錄:
bash:~$ cd linux
拷貝 inotify 補丁:
bash:~$ sudo cp ~/inotify* /usr/src
將核心打補丁:
bash:~$ sudo patch -p1 < ../inotify*.patch
構建核心:
bash:~$ sudo make menuconfig
像平時一樣配置您的核心,確保 inotify 工作正常。如果必要,請將新核心添加到引導載入程式中,但是一定要記住維護舊核心的映像和引導載入程式選項。這一步對於不同引導載入程式有所不同(請參閱 參考資料 瞭解關於特定引導載入程式的更多資訊)。重新引導電腦並選擇啟用 inotify 的新核心。在繼續往下操作前,測試您的新核心以確保它工作正常。
建立 inotify 裝置
接下來,您需要確保建立 /dev/inotify 裝置。以下步驟帶領您完成這個過程。重要注意:次裝置編號可能會發生改變,所以您需要多加註意以確保它隨時更新!如果 Linux 安裝支援 udev 功能,它將會自動保持更新。
在重新引導到新核心後,您必須擷取次裝置編號:
bash:~$ dmesg | grep ^inotify
返回結果樣本如下:
inotify device minor=63
因為 inotify 是 misc
裝置,所以主裝置編號是 10。要建立裝置節點作為根使用者,請執行以下命令:
bash:~$ mknod /dev/inotify c 10 63
注意:如有必要,請使用合適的次裝置編號替換“63”。
您可以隨意設定您想要的許可權。一個樣本使用權限設定如下所示:
bash:~$ chown root:root /dev/inotify
bash:~$ chmod 666 /dev/inotify
現在準備使用 inotify 裝置進行檔案系統監控。
在簡單應用程式中使用 inotify
為示範 inotify 的使用,我將展示如何為檔案系統事件構造一個監控任意目錄(或單個檔案)的樣本程式。我將站在一個較高的層次上來展示 inotify 使檔案系統監控變得多麼容易。
Main 方法
這個簡單的樣本向我們展示 inotify 在任意目錄上設定監控是多麼容易。稍後我們將看到主要的協助器常式。您可以在本文的 下載 一節擷取這些例子中使用的範例程式碼。
清單 1. 在目錄上設定監控
/* This program will take as argument a directory name and monitor it, printing event notifications to the console. */ int main (int argc, char **argv) { /* This is the file descriptor for the inotify device */ int inotify_fd; /* First we open the inotify dev entry */ inotify_fd = open_inotify_dev(); if (inotify_fd < 0) { return 0; } /* We will need a place to enqueue inotify events, this is needed because if you do not read events fast enough, you will miss them. */ queue_t q; q = queue_create (128); /* Watch the directory passed in as argument Read on for why you might want to alter this for more efficient inotify use in your app. */ watch_dir (inotify_fd, argv[1], ALL_MASK); process_inotify_events (q, inotify_fd); /* Finish up by destroying the queue, closing the fd, and returning a proper code */ queue_destroy (q); close_inotify_dev (inotify_fd); return 0; } |
重要的協助器方法
以下是每個基於 inotify 的應用程式共同的最重要的協助器常式:
- 為讀取而開啟 inotify 裝置。
- 對從該裝置讀取的事件進行排隊。
- 允許應用程式對事件通知進行有用處理的實際的每事件處理器。
我不會深入鑽研事件排隊的細節,因為我們能夠使用一些策略來避免排隊。提供的代碼中就展示了一個這樣的方法;更先進的多線程方法可以並且已經在其他地方實現。在那些實現中,讀者線程簡單地在 inotify 裝置上執行 select()
,然後將事件拷貝到一些線程共用的儲存空間(或者一些像 Glib 的非同步訊息佇列的東西),以後處理器線程會處理這裡的事件。
清單 2. 開啟 inotify 裝置
/* This simply opens the inotify node in dev (read only) */ int open_inotify_dev () { int fd; fd = open("/dev/inotify", O_RDONLY); if (fd < 0) { perror ("open(\"/dev/inotify\", O_RDONLY) = "); } return fd; } |
這對任何一個在 Linux 系統上進行過檔案編程的人來說都應該是熟悉的。
清單 3. 實際的事件處理常式
/* This method does the dirty work of determining what happened, then allows us to act appropriately */ void handle_event (struct inotify_event *event) { /* If the event was associated with a filename, we will store it here */ char * cur_event_filename = NULL; /* This is the watch descriptor the event occurred on */ int cur_event_wd = event->wd; if (event->len) { cur_event_filename = event->filename; } printf("FILENAME=%s\n", cur_event_filename); printf("\n"); /* Perform event dependent handler routines */ /* The mask is the magic that tells us what file operation occurred */ switch (event->mask) { /* File was accessed */ case IN_ACCESS: printf("ACCESS EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was modified */ case IN_MODIFY: printf("MODIFY EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File changed attributes */ case IN_ATTRIB: printf("ATTRIB EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was closed */ case IN_CLOSE: printf("CLOSE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was opened */ case IN_OPEN: printf("OPEN EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was moved from X */ case IN_MOVED_FROM: printf("MOVE_FROM EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was moved to X */ case IN_MOVED_TO: printf("MOVE_TO EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Subdir was deleted */ case IN_DELETE_SUBDIR: printf("DELETE_SUBDIR EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was deleted */ case IN_DELETE_FILE: printf("DELETE_FILE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Subdir was created */ case IN_CREATE_SUBDIR: printf("CREATE_SUBDIR EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was created */ case IN_CREATE_FILE: printf("CREATE_FILE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Watched entry was deleted */ case IN_DELETE_SELF: printf("DELETE_SELF EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Backing FS was unmounted */ case IN_UNMOUNT: printf("UNMOUNT EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Too many FS events were received without reading them some event notifications were potentially lost. */ case IN_Q_OVERFLOW: printf("Warning: AN OVERFLOW EVENT OCCURRED: \n"); break; case IN_IGNORED: printf("IGNORED EVENT OCCURRED: \n"); break; /* Some unknown message received */ default: printf ("UNKNOWN EVENT OCCURRED for file \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; } } |
在每一條 case
語句中,您可以隨意執行任意已實現並且滿足需要的方法。
至於效能監控,您可以確定哪些檔案是最經常被讀取的和它們開啟的期間。這種監控非常方便,因為在某些情況下,如果檔案在短時間內被應用程式重複地讀取,它會將檔案快取在記憶體中而不用返回磁碟去讀取,從而提高效能。
很容易舉出一些執行有趣操作的特定於事件的處理器的例子。比如,如果您是在為底層檔案系統實現一個中繼資料存放區索引,您可能會尋找檔案建立事件,不久還會在該檔案上觸發一個中繼資料挖掘操作。在安全環境中,如果檔案被寫入一個無人可以寫入的目錄,您會觸發某些形式的系統警報。
請注意,inotify 支援許多非常細粒度的事件 —— 例如 CLOSE
與 CLOSE_WRITE
。
本文中的代碼所列舉的許多事件,可能您並不希望在每次代碼運行時都看到。實際上,只要可能,您可以並且應該只請求對您的應用程式有用的事件子集。出於測試目的,本文章提供的代碼通過嚴格使用完整掩碼(如可下載的範例程式碼[請參閱 參考資料] 中 main
方法的第 51 行附近或者上面的 清單 1 中的第 29 行所執行的)展示了許多事件。應用程式員通常想要有更多選擇,而您則需要更特定的掩碼來滿足您的需要。這使您可以從上述的 handle_event()
方法中的 catch 語句刪除不感興趣的條目。
結束語
當應用於效能監控、調試和自動化領域時,inotify 是一種用於監控 Linux 檔案系統的、強大且細粒度的機制。使用本文提供的代碼,您就可以編寫能夠以最低的效能開銷響應或記錄檔案系統事件的應用程式。