Linux操作系统中的通知机制 - 监控文件事件 inotify

发布于:2025-05-09 ⋅ 阅读:(22) ⋅ 点赞:(0)

某些应用程序需要对文件或目录进行监控,已侦测其是否发生了特定事件。例如,当把文件加入或移出一目录时,图形化文件管理器应能判定此目录是否在其当前显示之列,而守护进程可能也想要监控自己的配置文件,以了解其是否被修改。
自内核 2.6.13 起,Linux 开始提供 inotify 机制,以允许应用程序监控文件事件。注意:inotify 机制所取代的是 dnotify,后者已经淘汰。inotify 和 dnotify 都是 Linux 专有机制。

1 概述

使用 inotify API 有以下几个关键步骤1

  1. 应用程序使用 inotify_init()来创建 inotify 实例,该系统调用所返回的文件描述符用于在后续操作中指代该实例。
  2. 应用程序使用 inotify_add_watch()向 inotify 实例(由步骤 1 创建)的监控列表添加条目,以告知内核哪些文件是自己的兴趣所在。每个监控项都包含一个路径名以及相关的位掩码。位掩码针对路径名指明了所要监控的事件集合。作为函数结果,inotify_add_watch()将返回一监控描述符,用于在后续操作中指代该监控项。(系统调用 inotify_rm_watch()执行其逆向操作,将之前添加入 inotify 实例的监控项移除。)
  3. 为获得事件通知,应用程序需针对 inotify 文件描述符执行 read()操作。每次对 read()的成功调用,都会返回一个或多个 inotify_event 结构,其中各自记录了处于 inotify 实例监控之下的某一路径名所发生的事件。
  4. 应用程序在结束监控时会关闭 inotify 文件描述符。这会自动清除与 inotify 实例相关的所有监控项。
    inotify 机制可用于监控文件或目录。当监控目录时,与路径自身及其所含文件相关的事件都会通知给应用程序。

inotify 监控机制为非递归。若应用程序有意监控整个目录子树内的事件,则需对该树中的每个目录发起 inotify_add_watch()调用。

可使用 select()、poll()、epoll 以及由信号驱动的 I/O来监控 inotify文件描述符。只要有事件可供读取,上述 API 便会将 inotify 文件描述符标记为可读。

2 inotify API

inotify_init()系统调用可创建一新的 inotify 实例。

#include <sys/inotify.h>
int inotify_init(void);
								/* Returns file descriptor on success, or -l on error */

作为函数结果,inotify_init()会返回一个文件描述符(句柄),用于在后续操作中指代此inotify 实例。

针对文件描述符 fd 所指代 inotify 实例的监控列表,系统调用 inotify_add_watch()既可以
追加新的监控项,也可以修改现有监控项。(请参考下图 :一个 inotify 实例及与之相关的内核数据结构。)

#include <sys/inotify.h>
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
								/* Returns watch descriptor on success, or -l on error */

参数 pathname 标识欲创建或修改的监控项所对应的文件。调用程序必须对该文件具有读权限(调用 inotify_add_watch()时,会对文件权限做一次性检查。只要监控项继续存在,即便有人更改了文件权限,使调用程序不再对文件具有读权限,调用程序依然会继续收到文件的通知消息)。
请添加图片描述
参数 mask 为一位掩码,针对 pathname 定义了意欲监控的事件。稍后会论及可在掩码中指定的各种位值。
如果先前未将 pathname 加入 fd 的监控列表,那么 inotify_add_watch()会在列表中创建一新的监控项,并返回一新的、非负监控描述符,用来在后续操作中指代此监控项。对 inotify 实例来说,该监控描述符是唯一的。
若先前已将 pathname 加入 fd 的监控列表,则 inotify_add_watch()会修改现有 pathname 监控项的掩码,并返回其监控描述符。(此描述符就是最初将 pathname 加入该监控列表的系统调用inotify_add_watch()所返回的监控描述符。)
系统调用 inotify_rm_watch()会从文件描述符 fd 所指代的 inotify 实例中,删除由 wd 所定义的监控项。

#include <sys/inotify.h>
int inotify_rm_watch(int fd,uint32_t wd);
											/* Returns 0 on success, or-l on error */

参数 wd 是一监控描述符,由之前对 inotify_add_watch()的调用返回。(uint32_t 数据类型为一无符号 32 位整数。)

删除监控项会为该监控描述符生成 IN_IGNORED 事件。

3 inotify 事件

使用 inotify_add_watch()删除或修改监控项时,位掩码参数 mask 标识了针对给定路径名(pathname)而要监控的事件。下表的“in”列列出了可在 mask 中定义的事件位。
在这里插入图片描述
在这里插入图片描述对于表中所列出的绝大多数位做一些细节的描述。

  • 当文件的元数据(比如,权限、所有权、链接计数、扩展属性、用户 ID 或组 ID 等)改变时,会发生 IN_ATTRIB 事件。
  • 删除受监控对象(即,一个文件或目录)时,发生 IN_DELETE_SELF 事件。当受监控对象是一个目录,并且该目录所含文件之一遭删除时,发生 IN_DELETE 事件。
  • 重命名受监控对象时,发生 IN_MOVE_SELF 事件。重命名受监控目录内的对象时,发生 IN_MOVED_FROM 和 IN_MOVED_TO 事件。其中,前一事件针对包含旧对象名的目录,后一事件则针对包含新对象名的目录。
  • IN_DONT_FOLLOW、IN_MASK_ADD、IN_ONESHOT 和 IN_ONLYDIR 位并非对监控事件的定义,而是意在控制 inotify_add_watch()系统调用的行为。
  • IN_DONT_FOLLOW 则规定,若 pathname 为符号链接,则不对其解引用。其作用在于令应用程序可以监控符号链接,而非符号连接所指代的文件。
  • 倘若对已为同一 inotify描述符所监控的同一路径名再次执行 inotify_add_watch()调用,那么默认情况下会用给定的 mask 掩码来替换该监控项的当前掩码。如果指定了IN_MASK_ADD,那么则会将 mask 值与当前掩码相或。
  • IN_ONESHOT 允许应用只监控 pathname 的一个事件。事件发生后,监控项会自动从监控列表中消失。
  • 只有当 pathname 为目录时,IN_ONLYDIR 才允许应用程序对其进行监控。如果pathname 并非目录,那么调用 inotify_add_watch()失败,报错为 ENOTDIR。如要确保监控对象为一目录,则使用该标志可以规避竞争条件的发生。

4 读取 inotify 事件

将监控项在监控列表中登记后,应用程序可用 read()从 inotify 文件描述符中读取事件,以判定发生了哪些事件。若时至读取时尚未发生任何事件,read()会阻塞下去,直至有事件产生(除非对该文件描述符设置了 O_NONBLOCK 状态标志,这时若无任何事件可读,read()将立即失败,并报错 EAGAIN)。
事件发生后,每次调用 read()会返回一个缓冲区,内含一个或多个如下类型的结构(请见下图:包含 3 个 inotify_event 结构的输入缓冲区):

struct inotify_event{
int	wd;				/* Watch descriptor on which event occurred */
uint32_t mask;		/* Bits describing event that occurred */
uint32_t cookie;	/* Cookie for related events(for rename())*/
uint32_t len;		/* Size of 'name'field */
char	name[];		/* Optional null-terminated filename */
};

请添加图片描述
字段 wd 指明发生事件的是那个监控描述符。该字段值由之前对 inotify_add_watch()的调用返回。当应用程序要监控同一 inotify 文件描述符下的多个文件和目录时,字段 wd 就派上用场。应用利用其所提供的线索来判定发生事件的特定文件或目录。(要做到这一点,应用程序必须维护专有数据结构,记录监控描述符与路径名之间的关系。)

mask 字段会返回描述该事件的位掩码。由第4节中表所示的 Out 列展示了可出现于 mask 中的位范围。还要注意下列与特殊位相关的更多细节。

  • 移除监控项时,会产生 IN_IGNORED 事件。起因可能有两个:其一,应用程序使用了inotify_rm_watch()系统调用显式移除监控项;其二,因受监控对象被删除或其所驻留的文件系统遭卸载,致使内核隐式删除监控项。以 IN_ONESHOT 而建立的监控项因事件触发而遭自动移除时,不会产生 IN_IGNORED 事件。
  • 如果事件的主体为路径,那么除去其他位以外,在 mask 中还会设置 IN_ISDIR 位。
  • IN_UNMOUNT 事件会通知应用程序包含受监控对象的文件系统已遭卸载。该事件发生之后,还会产生包含 IN_IGNORED 置位的附加事件。
  • 第5节将介绍 IN_Q_OVERFLOW,并讨论对排队 inotify 事件的限制。

使用 cookie 字段可将相关事件联系在一起。目前,只有在对文件重命名时才会用到该字段。当这种情况发生时,系统会针对待重命名文件所在目录产生 IN_MOVED_FROM 事件,然后,还会针对重命名后文件的所在目录生成 IN_MOVED_TO 事件。(若仅是在同一目录内为文件改名,系统则会针对同一目录产生上述两个事件。)两个事件的 cookie 字段值相等,故而应用程序得以将它们关联起来。

当受监控目录中有文件发生事件时,name 字段返回一个以空字符结尾的字符串,以标识该文件。若受监控对象自身有事件发生,则不使用 name 字段,将 len 字段置 0。

len 字段用于表示实际分配给 name 字段的字节数。在 read()所返回的缓冲区中,存储于name 内的字符串结尾与下一个 inotify_event 结构的开始(请参见 19.2 节)之间,可能会有额外填充字节,故而 len 字段不可或缺。单个 inotify 事件的长度是 sizeof(struct inotify_event)+ len。

如果传递给 read()的缓冲区过小,无法容纳下一个 inotify_event 结构,那么 read()调用将以失败告终,并以 EINVAL 错误向应用程序报告这一情况。应用程序可再次以更大的缓冲区执行 read()操作。然而,只要确保缓冲区足以容纳至少一个事件,这一问题将得以完全规避:传给 read()的缓冲区应至少为 sizeof(struct inotify_event)+ NAME_MAX + 1 字节,其中 NAME_MAX 是文件名的最大长度,此外在加上终止空字符使用的 1 个字节。

采用的缓冲区大小如大于最小值,则可自单个 read()中读取多个事件,效率极高。对 inotify文件描述符所执行的 read(),将在已发生事件数量与缓冲区可容纳事件数量间取最小值并返回之。

从 inotify 文件描述符中读取的事件形成了一个有序队列。打个比方,这样一来,对文件重命名时,便可保证在 IN_MOVED_TO 事件之前能读取到 IN_MOVED_FROM 事件。

在事件队列的末尾追加一个新事件时,如果此新事件与队列当前的尾部事件拥有相同的wd、mask、cookie 和 mask 值,那么内核会将两者合并(以避免对新事件排队)。之所以这么做,是因为很多应用程序都并不关注同一事件的反复出现,而丢弃多余的事件能降低内核维护事件队列所需的内存总量。然而,这也意味着使用 inotify 将无法可靠判定出周期性事件的发生次数或频率。

程序示例
虽然在前文中描述了 inotify API 的诸多细节,但实际上,该 API 使用起来却颇为简单。示例程序展示了对 inotify 的运用。

/* demo_inotify.c

   Demonstrate the use of the inotify API.
   运用 inotify API 
   Usage: demo_inotify pathname...

   The program monitors each of the files specified on the command line for all
   possible file events.

   This program is Linux-specific. The inotify API is available in Linux 2.6.13
   and later.
*/
#include <sys/inotify.h>
#include <limits.h>
#include "tlpi_hdr.h"

static void             /* Display information from inotify_event structure */
displayInotifyEvent(struct inotify_event *i)
{
    printf("    wd =%2d; ", i->wd);
    if (i->cookie > 0)
        printf("cookie =%4d; ", i->cookie);

    printf("mask = ");
    if (i->mask & IN_ACCESS)        printf("IN_ACCESS ");
    if (i->mask & IN_ATTRIB)        printf("IN_ATTRIB ");
    if (i->mask & IN_CLOSE_NOWRITE) printf("IN_CLOSE_NOWRITE ");
    if (i->mask & IN_CLOSE_WRITE)   printf("IN_CLOSE_WRITE ");
    if (i->mask & IN_CREATE)        printf("IN_CREATE ");
    if (i->mask & IN_DELETE)        printf("IN_DELETE ");
    if (i->mask & IN_DELETE_SELF)   printf("IN_DELETE_SELF ");
    if (i->mask & IN_IGNORED)       printf("IN_IGNORED ");
    if (i->mask & IN_ISDIR)         printf("IN_ISDIR ");
    if (i->mask & IN_MODIFY)        printf("IN_MODIFY ");
    if (i->mask & IN_MOVE_SELF)     printf("IN_MOVE_SELF ");
    if (i->mask & IN_MOVED_FROM)    printf("IN_MOVED_FROM ");
    if (i->mask & IN_MOVED_TO)      printf("IN_MOVED_TO ");
    if (i->mask & IN_OPEN)          printf("IN_OPEN ");
    if (i->mask & IN_Q_OVERFLOW)    printf("IN_Q_OVERFLOW ");
    if (i->mask & IN_UNMOUNT)       printf("IN_UNMOUNT ");
    printf("\n");

    if (i->len > 0)
        printf("        name = %s\n", i->name);
}

#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))

int
main(int argc, char *argv[])
{
    int inotifyFd, wd, j;
    char buf[BUF_LEN] __attribute__ ((aligned(8)));
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    if (argc < 2 || strcmp(argv[1], "--help") == 0)
        usageErr("%s pathname...\n", argv[0]);

    inotifyFd = inotify_init();                 /* Create inotify instance */
    if (inotifyFd == -1)
        errExit("inotify_init");

    /* For each command-line argument, add a watch for all events */

    for (j = 1; j < argc; j++) {
        wd = inotify_add_watch(inotifyFd, argv[j], IN_ALL_EVENTS);
        if (wd == -1)
            errExit("inotify_add_watch");

        printf("Watching %s using wd %d\n", argv[j], wd);
    }

    for (;;) {                                  /* Read events forever */
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0)
            fatal("read() from inotify fd returned 0!");

        if (numRead == -1)
            errExit("read");

        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        /* Process all of the events in buffer returned by read() */

        for (p = buf; p < buf + numRead; ) {
            event = (struct inotify_event *) p;
            displayInotifyEvent(event);

            p += sizeof(struct inotify_event) + event->len;
        }
    }

    exit(EXIT_SUCCESS);
}

示例程序将执行以下步骤。

  • 使用 inotify_init(),创建 inotify 文件描述符①。
  • 使用 inotify_add_watch(),将程序命令行参数中指定的每个文件加入监控项②。每个监控项都将监控所有可能发生的事件。
  • 执行无限循环。
    从 inotify 描述符读取事件缓冲区③。
    调用displayInotifyEvent()函数,以显示上述缓冲区中各inotify_event 结构的内容④。

以下 shell 会话演示了对示例程序的使用。首先,在后台运行该程序的实例,对两个目录进行监控。

$ mkdir dir1 dir2
$ ./demo_inotify dir1 dir2 &
[3] 2525817
Watching dir1 using wd 1
Watching dir2 using wd 2

然后,执行某些命令,从而在两个目录中产生事件。先使用 cat(1)创建一个文件:

$ cat > dir1/aaa
Read 64 bytes from inotify fd
    wd = 1; mask = IN_CREATE
        name = aaa
    wd = 1; mask = IN_OPEN
        name = aaa

由后台程序所生成的上述输出表明,read()读取了包含两个事件的缓冲区。继续在该文件中执行某些输入操作,然后输入 end-of-file 字符串:

hello world
Read 32 bytes from inotify fd
    wd = 1; mask = IN_MODIFY
        name = aaa
//type Control+D
Read 32 bytes from inotify fd
    wd = 1; mask = IN_CLOSE_WRITE
        name = aaa

接下来,将该文件转移至另一个受监控的目录,同时对其重新命名。这会产生两个事件,一个对应于文件的源目录(监控描述符 1),另一个对应于文件的目标目录(监控描述符 2)。

$ mv dir1/aaa dir2/bbb
Read 64 bytes from inotify fd
    wd = 1; cookie =66524082; mask = IN_MOVED_FROM
        name = aaa
    wd = 2; cookie =66524082; mask = IN_MOVED_TO
        name = bbb

以上两个事件共享相同的 cookie 值,允许应用程序将它们联系起来。
当在其中一个受监控目录下创建子目录时,由此产生的事件掩码会置 IN_ISDIR 位,以示该事件的对象是一目录。

$ mkdir dir2/ddd
Read 32 bytes from inotify fd
    wd = 2; mask = IN_CREATE IN_ISDIR
        name = ddd

此处,再次提醒大家,inotify 监控是非递归的。如果应用程序有意对新创建的子目录进行监控,则需进一步执行 inotify_add_watch()系统调用,并指明子目录的路径名。
最后,将其中一个受监控目录删除:

$ rmdir dir1/
Read 32 bytes from inotify fd
    wd = 1; mask = IN_DELETE_SELF
    wd = 1; mask = IN_IGNORED

系统会生成最后一个事件,以通知应用程序,内核已从监控列表中删除该监控项。

5 队列限制和/proc 文件

对 inotify 事件做排队处理,需要消耗内核内存。正因如此,内核会对 inotify 机制的操作施以各种限制。超级用户可配置/proc/sys/fs/inotify 路径中的 3 个文件来调整这些限制

  • max_queued_events
    调用 inotify_init()时,使用该值来为新 inotify 实例队列中的事件数量设置上限。一旦超出这一上限,系统将生成 IN_Q_OVERFLOW 事件,并丢弃多余的事件。溢出事件的 wd 字段值为−1。
  • max_user_instances
    对由每个真实用户 ID 创建的 inotify 实例数的限制值。
  • max_user_watches
    对由每个真实用户 ID 创建的监控项数量的限制值。

这 3 个文件的典型默认值分别为 16384、128 和 8192。


  1. inotify 机制属可选的 Linux 内核组件,可通过 CONFIG_INOTIFY 和 CONFIG_ INOTIFY_USER 选项进行配置。 ↩︎


网站公告

今日签到

点亮在社区的每一天
去签到