1. 程式人生 > 其它 >linux 檔案監控之 inotify

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系統程式設計手冊>> 這本書,同時在專案中有需要使用檔案監聽機制的場景。所以就詳細瞭解了檔案事件監聽的概念和具體用法。希望通過撰寫本文幫自己理清思路,加深對這個知識點的理解,也希望能夠對正在瞭解這一塊的同學有所幫助。