linux 文件監控之 inotify


某些應用程序需要對文件或目錄進行監控,以感知這些文件或目錄發生了特定事件。在 Linux 中提供了 inotify 機制允許應用程序可以監聽文件(目錄)事件。

本文主要從以下幾個方面對 inotify 進行介紹:

  • inotify 使用場景
  • inotify 機制關聯的相關系統調用
  • inotify 支持的事件類型
  • inotify 使用示例

使用場景

監聽文件或者目錄的變更,最終目的一定是基於不同的變更事件采取相對應的處理措施。比較常見的使用場景如下:

  • 配置文件熱加載,當配置文件發生變化時進程可以自動感知並重新 reload 配置文件,如 golang 的明星項目--viper
  • 配置保持功能,當我們需要保持服務器上某些文件不被改動時,可以監聽需要保持的文件。當文件出現變更時做相應的恢復處理
  • 當文件移出或者加入到某個目錄下的時候,圖形化文件管理器需要根據對應的事件作出相對應的調整

系統調用

與 inotify 有關的系統調用主要有三個:inotify_initinotify_add_watchinotify_rm_watch,具體的系統調用如下所示:

inotify_init

#include <sys inotify.h="">

int inotify_init(void);

inotify_init創建一個inotify實例,該函數會返回文件描述符用來指代inotify實例,同時之后需要通過對該文件描述符進行read 操作獲取文件變更事件

inotify_add_watch

#include <sys inotify.h="">

int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
  • fd 指代 inotify_init 系統調用返回的 notify 實例
  • pathname 指代需要被監聽事件的文件或者目錄路徑
  • mask 事件掩碼,表明需要監聽的事件類型。具體的事件類型下文會進行描述

該系統調用返回值(wd)是監控描述符,指代一條監控項。

上圖展示了一個notify實例,以及該實例維護的一組監控項

  • 監控描述符就是 inotify_add_watch系統調用的返回,唯一指代一個監控描述項
  • 掩碼即是 mask 用來定義具體的監聽事項
  • pathname 即是完整的待監聽文件或目錄的合法路徑

inotify_rm_watch

#include <sys inotify.h="">

int inotify_rm_watch(int fd, uint32_t wd);
  • fd 指代 inotify_init 系統調用返回的 notify 實例
  • wd 指代監控項描述符

事件類型

常規事件類型

mask 標志 描述
IN_ACCESS 文件被訪問(執行了 read 操作)
IN_ATTRIB 文件元數據發生變更
IN_CLOSE_WRITE 關閉為了寫入而打開的文件
IN_CLOSE_NOWRITE 關閉以只讀方式打開的文件
IN_CREATE 在受控目錄內創建了文件或者目錄
IN_DELETE 在受控目錄內刪除了文件或者目錄
IN_DELETE_SELF 刪除受控文件或者目錄本身
IN_MOVED_FROM 文件移出受控目錄之外
IN_MOVED_TO 文件移入受控目錄
IN_OPEN 文件被打開
IN_MOVE IN_MOVED_FROM|IN_MOVED_TO 事件的統稱
IN_CLOSE IN_CLOSE_WRITE|IN_CLOSE_NOWRITE 統稱
  • IN_ATTRIB監控的元數據變更包括,權限,所有權,鏈接數,用戶 ID 等
  • 重命名受控對象時會發出IN_DELETE_SELF事件,而如果受控目標是一個目錄,那么受控目標下的文件發生重命名時會觸發兩個事件IN_MOVED_FROMIN_MOVED_TO

在我們的日常開發工作中,上述事件已經基本涵蓋了文件變更的所有情況。我們可以按照各自的場景,針對上述不同的事件類型做出相應的處理流程。

其他事件

除了上述文件的常規事件外,inotify還提供了以下幾個 mask 來控制事件監聽的過程

mask 標志 描述
IN_DONT_FOLLOW 不對符號鏈接引用
IN_MASK_ADD 將事件追加到 pathname 的當前監控掩碼
IN_ONESHOT 只監控 pathname 的一個事件
IN_ONLYDIR pathname 不為目錄時會失敗

將上述 mask 標志添加到 inotify_add_watch 中時可以控制監聽過程,這么說有點籠統,舉個例子來說。

inotify_add_watch(fd, pathname, IN_OPEN | IN_CLOSE | IN_ONESHOT);

上面這段代碼,除了監聽文件的 IN_OPENIN_CLOSE事件外,還添加了 IN_ONESHOT mask,那么這就意味着,當監聽到 pathname 所指代的文件一次事件后 inotify就不會在監聽 pathname 所指代的文件發出的事件了。

上述 mask 是在添加某個文件監控項的時候作為inotify_add_watch系統調用的參數傳入的。除此之外還有以下幾個事件,這些事件不需要用戶顯示調用inotify_add_watch添加,僅當出現一些其他異常情況時發出。

mask 標志 描述
IN_IGNORED 監控項為內核或者應用程序移除
IN_ISDIR 被監聽的是一個目錄的路徑
IN_Q_OVERFLOW 事件隊列溢出
IN_UNMOUNT 包含對象的文件系統遭到卸載
  • IN_ISDIR事件表明被監聽的 pathname 指代的是一個目錄,舉例來說 mkdir /tmp/xxx 這個系統命令會產生 IN_CREATE|IS_DIR 事件。

事件結構

上文描述了inotify支持的事件類型,可以看出來支持的事件類型非常豐富,基本滿足了我們對於文件監聽的各種訴求。除了上述的事件類型外,在這一小節我們會簡單描述一下inotifyevent結構,通過事件的數據結構可以看出,從事件中我們可以獲取到哪些信息。事件的具體數據結構如下:

struct inotify_event {
        int		wd;  		//監控描述符號,唯一指代一個監控項目
  	uint32_t	mask;	//監控的事件類型
  	uint32_t	cookie;	//只有重命名才會使用到該字段
  	uint32_t	len;		//下面 name 數組的尺寸
  	char		name[];	//當受控目錄下的文件有變更時,該字符串會記錄發生變更的文件的文件名
};

使用示例

inotify demo 該示例注釋非常詳細,同時使用到了上述的三個系統調用,具體代碼如下:

#include <errno.h>
       #include <poll.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <sys inotify.h="">
       #include <unistd.h>
       #include <string.h>

       /* Read all available inotify events from the file descriptor 'fd'.
          wd is the table of watch descriptors for the directories in argv.
          argc is the length of wd and argv.
          argv is the list of watched directories.
          Entry 0 of wd and argv is unused. */
			
       static void
       handle_events(int fd, int *wd, int argc, char* argv[])
       {
           /* Some systems cannot read integer variables if they are not
              properly aligned. On other systems, incorrect alignment may
              decrease performance. Hence, the buffer used for reading from
              the inotify file descriptor should have the same alignment as
              struct inotify_event. */

           char buf[4096]
               __attribute__ ((aligned(__alignof__(struct inotify_event))));
           const struct inotify_event *event;
           ssize_t len;

           /* Loop while events can be read from inotify file descriptor. */

           for (;;) {

               /* Read some events. */
							// fd 為 inotfy 實例文件描述符
               len = read(fd, buf, sizeof(buf)); //讀取事件
               if (len == -1 && errno != EAGAIN) {
                   perror("read");
                   exit(EXIT_FAILURE);
               }

               /* If the nonblocking read() found no events to read, then
                  it returns -1 with errno set to EAGAIN. In that case,
                  we exit the loop. */
							
               if (len <= 0)
                   break;

               /* Loop over all events in the buffer. */

               for (char *ptr = buf; ptr < buf + len;
                       ptr += sizeof(struct inotify_event) + event->len) {

                   event = (const struct inotify_event *) ptr;

                   /* Print event type. */
									//通過事件 mask 掩碼獲取當前事件是哪一類型的事件
                   if (event->mask & IN_OPEN)
                       printf("IN_OPEN: ");
                   if (event->mask & IN_CLOSE_NOWRITE)
                       printf("IN_CLOSE_NOWRITE: ");
                   if (event->mask & IN_CLOSE_WRITE)
                       printf("IN_CLOSE_WRITE: ");

                   /* Print the name of the watched directory. */

                   for (int i = 1; i < argc; ++i) {
                       if (wd[i] == event->wd) {
                           printf("%s/", argv[i]);
                           break;
                       }
                   }

                   /* Print the name of the file. */

                   if (event->len)
                       printf("%s", event->name);

                   /* Print type of filesystem object. */

                   if (event->mask & IN_ISDIR)
                       printf(" [directory]\n");
                   else
                       printf(" [file]\n");
               }
           }
       }

       int
       main(int argc, char* argv[])
       {
           char buf;
           int fd, i, poll_num;
           int *wd;
           nfds_t nfds;
           struct pollfd fds[2];

           if (argc < 2) {
               printf("Usage: %s PATH [PATH ...]\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           printf("Press ENTER key to terminate.\n");

           /* Create the file descriptor for accessing the inotify API. */

           fd = inotify_init1(IN_NONBLOCK); //創建 inotify 實例,fd文件句柄指代 inotify 實例
           if (fd == -1) {
               perror("inotify_init1");
               exit(EXIT_FAILURE);
           }

           /* Allocate memory for watch descriptors. */

           wd = calloc(argc, sizeof(int));
           if (wd == NULL) {
               perror("calloc");
               exit(EXIT_FAILURE);
           }

           /* Mark directories for events
              - file was opened
              - file was closed */
					//添加文件監控項,這里支持注冊多個監聽目錄,同時只監聽了 IN_OPEN 和 IN_CLOSE事件類型
           for (i = 1; i < argc; i++) {
               wd[i] = inotify_add_watch(fd, argv[i],
                                         IN_OPEN | IN_CLOSE);
               if (wd[i] == -1) {
                   fprintf(stderr, "Cannot watch '%s': %s\n",
                           argv[i], strerror(errno));
                   exit(EXIT_FAILURE);
               }
           }

           /* Prepare for polling. */
					//同時監聽終端個 inotify 消息,當終端回車時,該監聽程序退出,當監聽到文件變更事件后處理事件
           nfds = 2;
					
           fds[0].fd = STDIN_FILENO;       /* Console input */
           fds[0].events = POLLIN;

           fds[1].fd = fd;                 /* Inotify input */
           fds[1].events = POLLIN;

           /* Wait for events and/or terminal input. */

           printf("Listening for events.\n");
           while (1) {
               poll_num = poll(fds, nfds, -1);
               if (poll_num == -1) {
                   if (errno == EINTR)
                       continue;
                   perror("poll");
                   exit(EXIT_FAILURE);
               }

               if (poll_num > 0) {

                   if (fds[0].revents & POLLIN) {

                       /* Console input is available. Empty stdin and quit. */

                       while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
                           continue;
                       break;
                   }

                   if (fds[1].revents & POLLIN) {

                       /* Inotify events are available. */
					
                       handle_events(fd, wd, argc, argv);
                   }
               }
           }

           printf("Listening for events stopped.\n");

           /* Close inotify file descriptor. */

           close(fd);

           free(wd);
           exit(EXIT_SUCCESS);
       }

其他細節

inotify的事件隊列並不是無限大的,因為隊列也是需要消耗內核內存的,因此會設置一些上限加以限制,具體的配置可以通過修改對/proc/sys/fs/inotify下的三個文件來達到控制文件事件監聽的目的。

  • max_queued_events: 規定了 inotify事件隊列的數量上限,一旦超出這個限制,系統就會生成IN_Q_OVERFLOW事件,該事件上文有詳細描述,這里不再贅述。
  • max_user_instances:對由每個真實用戶 ID 創建的inotify實例數的限制值。
  • max_user_watchers:對由每個真實用戶 ID 創建的監控項數量的限制值。

總結

最近在學習 <<linux/unix系統編程手冊>> 這本書,同時在項目中有需要使用文件監聽機制的場景。所以就詳細了解了文件事件監聽的概念和具體用法。希望通過撰寫本文幫自己理清思路,加深對這個知識點的理解,也希望能夠對正在了解這一塊的同學有所幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM