使用 inotify 監控 Linux 文件系統事件

Inotify 是文件系統事件監控機制,計劃包含在即將發佈的 Linux 內核中作爲 dnotify 的有效替代。dnotify 是較早內核支持的文件監控機制。Inotify一種強大的、細粒度的、異步的機制,它滿足各種各樣的文件監控需要,不僅限於安全和性能。下面讓我們一起學習如何安裝 inotify 和如何構建一個示例用戶空間應用程序來響應文件系統事件。
文件系統事件監控對於從文件管理器到安全工具的各種程序都是必要的,但是 dnotify(早期內核中的標準)存在一些侷限性,這使我們期待出現一種更加完善的機制。抱着這種期待,我們發現了 inotify,一種更加現代化的文件系統事件監控替代品。
使用 inotify 取代 dnotify 的原因有很多。第一個原因是,dnotify 需要您爲每個打算監控是否發生改變的目錄打開一個文件描述符。當同時監控多個目錄時,這會消耗大量的資源,因爲有可能達到每個進程的文件描述符限制。
除此之外,文件描述符會鎖定目錄,不允許卸載(unmount)支持的設備,這在存在可移動介質的環境中會引發問題。在使用 inotify 時,如果正在監控被卸載的文件系統上的文件,那麼監控會被自動移除並且您會接收到一個卸載事件。
dnotify 不如 inotify 的第二個原因是 dnotify 有點複雜。注意,使用 dnotify 基礎設施的簡單文件系統監控粒度只停留於目錄級別。爲了使用 dnotify 進行更細粒度的監控,應用程序編程人員必須爲每個受監控的目錄保留一個 stat 結構的緩存。該用戶空間的 stat 結構緩存需要用來明確確定當接收到通知信號時目錄發生了什麼變化。當獲得通知信號時,生成 stat 結構列表並與最新的狀態相比較。顯而易見,這種技術是不理想的。
inotify 的另一個優點是它使用文件描述符作爲基本接口,使應用程序開發者使用 selectpoll 來監控設備。這允許有效的多路 I/O 和與 Glib 的 mainloop 的集成。相反,dnotify 所使用的信號常常使程序員頭疼並且感覺不太優雅。
inotify 通過提供一個更優雅的 API 解決了這些問題,該 API 使用最少的文件描述符,並確保更細粒度的監控。與 inotify 的通信是通過設備節點提供的。基於以上原因,對於監控 Linux 2.6 平臺上的文件,inotify 是您最明智的選擇。




回頁首


安裝 inotify 的第一步是確定您使用的 Linux 內核是否支持它。檢查發行版的最簡單方法是,尋找是否存在 /dev/inotify 設備。如果存在該設備,您可以跳到 
在撰寫本文時,inotify 包含在 Andrew Morton 的 Linux 2.6-mm 目錄樹中,而且一些 Linux 發行版正在提供支持 inotify 的內核(包括 Gentoo 和 Ubuntu)或者具有提供支持的補充內核包(例如 Fedora 和 SuSE)。因爲 Andrew 可能會根據需要從目錄樹刪除對 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 的新內核。在繼續往下操作前,測試您的新內核以確保它工作正常。
接下來,您需要確保創建 /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 在任意目錄上設置監控是多麼容易。稍後我們將看到主要的幫助器例程。您可以在本文的  一節獲取這些例子中使用的示例代碼。

清單 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 支持許多非常細粒度的事件 —— 例如 CLOSECLOSE_WRITE
本文中的代碼所列舉的許多事件,可能您並不希望在每次代碼運行時都看到。實際上,只要可能,您可以並且應該只請求對您的應用程序有用的事件子集。出於測試目的,本文章提供的代碼通過嚴格使用完整掩碼(如可下載的示例代碼[請參閱] 中 main 方法的第 51 行附近或者上面的清單1 中的第 29 行所執行的)展示了許多事件。應用程序員通常想要有更多選擇,而您則需要更特定的掩碼來滿足您的需要。這使您可以從上述的 handle_event() 方法中的 catch 語句刪除不感興趣的條目。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章