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系统编程手册>> 这本书,同时在项目中有需要使用文件监听机制的场景。所以就详细了解了文件事件监听的概念和具体用法。希望通过撰写本文帮自己理清思路,加深对这个知识点的理解,也希望能够对正在了解这一块的同学有所帮助。

posted @ 2021-10-31 20:39  jssyjam  阅读(5732)  评论(0编辑  收藏  举报