inotify 是 Linux 平台特有的一种文件系统事件监视机制。inotify API 提供了一种监视文件系统事件的机制。Inotify 可以被用于监视单独的文件,或监视目录。当监视一个目录时,inotify 将返回目录本身的事件,以及目录内的文件的事件。这组 API 使用了如下这些系统调用:
inotify_init(2) 创建一个 inotify 实例并返回一个文件描述符指向该 inotify 实例。更近一些的 inotify_init1(2) 与 inotify_init(2) 类似,但它可以带一个标记参数提供对一些额外功能的访问。
inotify_add_watch(2) 管理与 inotify 实例关联的 “观察列表”。观察列表中的每个项 (“watch”) 指定一个文件或目录的路径,以及内核应该监控的路径指向的文件上发生的事件的集合。inotify_add_watch(2) 创建一个新的观察项,或修改一个已经存在的观察。每个观察具有一个唯一的 “观察描述符”,当观察创建时 inotify_add_watch(2) 返回的一个整数。
当监测的文件或目录的事件发生时,这些事件将以结构化数据的形式为应用程序所用,这些事件可以使用 read(2) 从 inotify 文件描述符读出。
inotify_rm_watch(2) 从一个 inotify 观察列表中移除一个项。
当指向 inotify 实例的所有文件描述符都已经被关闭了(使用 close(2)),底层对象及其资源将被内核释放并重用;所有相关的观察也被自动地释放。
通过细心的编程,应用程序可以使用 inotify 高效地监视和缓存一系列文件系统对象的状态。然而,健壮的应用程序应该允许这样一个事实,即监控逻辑的缺陷,或下面描述的那种竞争可能会使缓存与文件系统状态不一致。执行一些一致性检查,并在探测到不一致时重建缓存可能是明智的。
为了确定发生了什么事件,应用程序可以使用 read(2) 从 inotify 文件描述符读取事件。如果截止目前还没有事件发生,则,假设是一个阻塞的文件描述符,read(2) 将阻塞直到至少有一个事件发生(除非被一个信号中断,在那种情况下调用失败,错误码为 EINTR)。
每个成功的 read(2) 调用返回一个包含了一个或多个如下结构的缓冲区:
struct inotify_event {
int wd; /* Watch descriptor */
uint32_t mask; /* Mask describing event */
uint32_t cookie; /* Unique cookie associating related
events (for rename(2)) */
uint32_t len; /* Size of name field */
char name[]; /* Optional null-terminated name */
};
wd 是这个事件发生的观察描述符。它是前面调用 inotify_add_watch(2) 返回的观察描述符中的一个。
mask 包含描述了发生的事件的位。
cookie 是连接相关事件的唯一的整数。目前,它只被用于重命名事件,并允许应用程序将最终的 IN_MOVED_FROM 和 IN_MOVED_TO 事件对连接起来。对于所有其它的事件类型,cookie 被设置为 0。
name 字段只有当返回的是观察的目录内的一个文件的事件时才出现;它表示观察的目录内的文件名。这个文件名是 null 结尾的,且可能包含更多的 null 字节 (’\0’) 以对齐后续的读到一个合适的地址边界。
len 字段表示 name 中所有字节的个数,包括 null 字节;因此每个 inotify_event 结构的长度为 sizeof(struct inotify_event) + len。
给 read(2) 的缓冲区太小以至于无法返回下一个事件相关的信息时的行为依赖于内核版本:在 2.6.21 之前的内核中,read(2) 返回 0;自内核 2.6.21 开始,read(2) 读取失败,错误码为 EINVAL。指定缓冲区大小为 sizeof(struct inotify_event) + NAME_MAX + 1
则最起码读取一个事件是足够的。
inotify_add_watch(2) 的 mask 参数,和读取一个 inotify 文件描述符时返回的 inotify_event 结构的 mask 字段都是表示 inotify 事件的位掩码。当调用 inotify_add_watch(2) 时可以在 mask 中指定,或者 read(2) 返回的 mask 字段中可能返回的,有如下位:
IN_ACCESS (+) 文件被访问(比如,read(2),execve(2))。
IN_ATTRIB (*) 元数据被修改 - 比如,权限 (如,chmod(2)),时间戳 (如,utimensat(2)),扩展属性 (setxattr(2)),链接计数 (自 Linux 2.6.25;比如,作为 link(2) 和 unlink(2) 的目标),以及用户/组 ID (比如,chown(2))。
IN_CLOSE_WRITE (+) 为写入而打开的文件已关闭。
IN_CLOSE_NOWRITE (*) 为文件或目录的非写入打开已关闭。
IN_CREATE (+) 在观察的目录中创建了文件/目录 (比如,open(2) O_CREAT,mkdir(2),link(2),symlink(2),UNIX 域 socket 上的 bind(2))。
IN_DELETE (+) 从监视目录中删除的文件/目录。
IN_DELETE_SELF 观察的文件/目录本身被删除。(这个事件也发生在一个对象被移动到另一个文件系统时,因为 mv(1) 在效果上等于将文件拷贝到另一个文件系统,然后将它从最初的文件系统中删除)。此外,后续还会为观察描述符生成一个 IN_IGNORED 事件。
IN_MODIFY (+) 文件别修改 (比如,write(2),truncate(2))。
IN_MOVE_SELF 观察的文件/目录本身被移动。
IN_MOVED_FROM (+) 当一个文件被重命名时目录包含旧的文件名时生成。
IN_MOVED_TO (+) 当一个文件被重命名时目录包含新的文件名时生成。
IN_OPEN (*) 文件或目录被打开。
Inotify 监视是基于 inode 的:当监视一个文件时(但监视包含一个文件的目录不是),对于文件的任何链接(在相同或不同的目录中)上的活动都会生成一个事件。
当监视一个目录时:
上面标有星号 (*) 的事件既可以发生在目录本身,也可以发生在目录内的对象上;以及
标有加号 (+) 的事件仅发生在目录内的对象(而不是目录本身)。
注意:监视目录时,如果事件是通过位于被监视目录之外的路径名(即链接)执行的,则不会为目录内的文件生成事件。
当为被监视目录中的对象生成事件时,返回的 inotify_event 结构中的 name 字段标识目录中文件的名称。
IN_ALL_EVENTS 宏被定义为所有上述事件的位掩码。这个宏也可以在调用 inotify_add_watch(2) 时用作掩码参数。
还定义了两个额外的便利宏:
IN_MOVE 等于 IN_MOVED_FROM | IN_MOVED_TO。
IN_CLOSE 等于 IN_CLOSE_WRITE | IN_CLOSE_NOWRITE。
调用 inotify_add_watch(2) 时,还可以在 mask 中指定以下位:
IN_DONT_FOLLOW (自 Linux 2.6.15) 如果它是符号链接,就不要解引用路径名。
IN_EXCL_UNLINK (自 Linux 2.6.36) 默认情况下,在观察目录的子文件或目录的事件时,即使子文件或目录已从目录中取消链接,也会为子文件或目录生成事件。对于某些应用程序,这可能会导致大量不感兴趣的事件(例如,如果观察 /tmp,其中许多应用程序会创建名称将立即被取消链接的临时文件)。指定 IN_EXCL_UNLINK 改变默认行为,以使被观察的目录的子目录或文件被从被观察目录取消链接后不会再生成事件。
IN_MASK_ADD 如果对应于路径名的文件系统对象的监视实例已经存在,则将掩码中的事件添加(或)到监视掩码(而不是替换掩码); 如果还指定了 IN_MASK_CREATE,则会导致错误 EINVAL。
IN_ONESHOT 监视路径名对应的文件系统对象的一个事件,然后从监视列表中删除。
IN_ONLYDIR (自 Linux 2.6.15) 仅当它是目录时才观察路径名; 如果路径名不是目录,则会导致错误 ENOTDIR。 使用此标志为应用程序提供了一种确保被监视对象是目录的无竞争方式。
IN_MASK_CREATE (自 Linux 4.18) 仅当它还没有与之关联的监视时才监视路径名; 如果路径名已被监视,则会导致错误 EEXIST。
使用此标志为应用程序提供了一种确保新监视不会修改现有监视的方法。 这很有用,因为多个路径可能引用同一个 inode,并且在没有此标志的情况下多次调用 inotify_add_watch(2) 可能会破坏现有的监视掩码。
在 read(2) 返回的 mask 字段中可能设置以下位:
IN_IGNORED 监视被显式删除(inotify_rm_watch(2))或自动删除(文件被删除,或文件系统被卸载)。
IN_ISDIR 此事件的主题是一个目录。
IN_Q_OVERFLOW 事件队列溢出(此事件的 wd 为 -1)。
IN_UNMOUNT 包含被监视对象的文件系统已卸载。 此外,随后将为监视描述符生成一个 IN_IGNORED 事件。
假设应用程序正在监视目录 dir 和文件 dir/myfile 中的所有事件。 下面的示例显示了将为这两个对象生成的一些事件。
fd = open("dir/myfile", O_RDWR);
为 dir 和 dir/myfile 生成 IN_OPEN 事件。
read(fd, buf, count);
为 dir 和 dir/myfile 生成 IN_ACCESS 事件。
write(fd, buf, count);
为 dir 和 dir/myfile 生成 IN_MODIFY 事件。
fchmod(fd, mode);
为 dir 和 dir/myfile 生成 IN_ATTRIB 事件。
close(fd);
为 dir 和 dir/myfile 生成 IN_CLOSE_WRITE 事件。
假设一个应用程序正在监视目录 dir1 和 dir2,以及文件 dir1/myfile。 以下示例显示了可能生成的一些事件。
link("dir1/myfile", "dir2/new");
为 myfile 生成一个 IN_ATTRIB 事件,为 dir2 生成一个 IN_CREATE 事件。
rename("dir1/myfile", "dir2/myfile");
为 dir1 生成一个 IN_MOVED_FROM 事件,为 dir2 生成一个 IN_MOVED_TO 事件,为 myfile 生成一个 IN_MOVE_SELF 事件。 IN_MOVED_FROM 和 IN_MOVED_TO 事件将具有相同的 cookie 值。
假设 dir1/xx 和 dir2/yy 是(唯一的)指向同一个文件的链接,并且应用程序正在监视 dir1、dir2、dir1/xx 和 dir2/yy。 按照下面给出的顺序执行以下调用将生成以下事件:
unlink("dir2/yy");
为 xx 生成一个 IN_ATTRIB 事件(因为它的链接计数发生变化),为 dir2 生成一个 IN_DELETE 事件。
unlink("dir1/xx");
为 xx 生成 IN_ATTRIB、IN_DELETE_SELF 和 IN_IGNORED 事件,为 dir1 生成 IN_DELETE 事件。
假设一个应用程序正在监视目录 dir 和(空的)目录 dir/subdir。 以下示例显示了可能生成的一些事件。
mkdir("dir/new", mode);
为 dir 生成一个 IN_CREATE | IN_ISDIR 事件。
rmdir("dir/subdir");
为 subdir 生成 IN_DELETE_SELF 和 IN_IGNORED 事件,为 dir 生成一个 IN_DELETE | IN_ISDIR 事件。
以下接口可用于限制 inotify 消耗的内核内存量:
/proc/sys/fs/inotify/max_queued_events
当应用程序调用 inotify_init(2) 设置可以排队到相应 inotify 实例的事件数上限时,将使用此文件中的值。超过此限制的事件将被丢弃,但始终会生成 IN_Q_OVERFLOW 事件。
/proc/sys/fs/inotify/max_user_instances
这指定了每个真实用户 ID 可以创建的 inotify 实例数的上限。
/proc/sys/fs/inotify/max_user_watches
这指定了每个真实用户 ID 可以创建的监视数量的上限。
Inotify 被合并到 2.6.13 Linux 内核中。 所需的库接口已添加到 glibc 2.4 版中。(在 glibc 2.5 版中添加了 IN_DONT_FOLLOW、IN_MASK_ADD 和 IN_ONLYDIR。)
Inotify API 是 Linux 特有的。
可以使用 select(2)、poll(2) 和 epoll(7) 监视 Inotify 文件描述符。 当事件可用时,文件描述符指示为可读。
从 Linux 2.6.25 开始,信号驱动的 I/O 通知可用于 inotify 文件描述符; 请参阅 fcntl(2) 中对 F_SETFL(用于设置 O_ASYNC 标志)、F_SETOWN 和 F_SETSIG 的讨论。传递给信号处理程序的 siginfo_t 结构(在 sigaction(2) 中描述)具有以下字段集: si_fd 设置为 inotify 文件描述符编号; si_signo 设置为信号编号; si_code 设置为 POLL_IN; 并且 POLLIN 设置在 si_band 中。
如果在 inotify 文件描述符上产生的连续输出 inotify 事件是相同的(相同的 wd、掩码、cookie 和名称),则如果尚未读取较旧的事件,则它们将合并为单个事件。这减少了事件队列所需的内核内存量,但也意味着应用程序不能使用 inotify 来可靠地计算文件事件。
从 inotify 文件描述符读取返回的事件形成有序队列。因此,例如,可以保证当从一个目录重命名到另一个目录时,事件将在 inotify 文件描述符上以正确的顺序生成。
通过 inotify 文件描述符监视的监视描述符集可以通过进程的 /proc/[pid]/fdinfo 目录中的 inotify 文件描述符条目查看。有关详细信息,请参阅 proc(5)。 FIONREAD ioctl(2) 返回可从 inotify 文件描述符读取的字节数。
Inotify API 不提供关于触发 inotify 事件的用户或进程的信息。特别是,对于通过 inotify 监视事件的进程来说,没有简单的方法来区分它自己触发的事件和其他进程触发的事件。
Inotify 只报告用户空间程序通过文件系统 API 触发的事件。因此,它不捕获发生在网络文件系统上的远程事件。(应用程序必须退回到轮询文件系统以捕获此类事件。)此外,各种伪文件系统(例如 /proc、/sys 和 /dev/pts)无法使用 inotify 进行监控。
Inotify API 不会报告由于 mmap(2)、msync(2) 和 munmap(2) 而可能发生的文件访问和修改。
Inotify API 通过文件名识别受影响的文件。但是,当应用程序处理 inotify 事件时,文件名可能已经被删除或重命名。
Inotify API 通过监视描述符识别事件。缓存监视描述符和路径名之间的映射(如果需要)是应用程序的责任。请注意,目录重命名可能会影响多个缓存路径名。
Inotify 监视目录不是递归的:要监视目录下的子目录,必须创建额外的监视。对于大型目录树,这可能会花费大量时间。
如果监视整个目录子树,并且在该树中创建了一个新的子目录,或者将现有目录重命名到该树下,请注意,当你为新子目录创建监视时,新文件(和子目录)可能已经存在子目录里面了。因此,你可能希望在添加监视后立即扫描子目录的内容(如果需要,可以递归地为其包含的任何子目录添加监视)。
请注意,事件队列可能会溢出。在这种情况下,事件会丢失。健壮的应用程序应该优雅地处理丢失事件的可能性。例如,可能需要重建部分或全部应用程序缓存。(一种简单,但可能代价高昂的方法是关闭 inotify 文件描述符,清空缓存,创建新的 inotify 文件描述符,然后为要监视的对象重新创建监视和缓存条目。)
如果文件系统挂载在受监视目录的顶部,则不会生成任何事件,并且不会为新挂载点下的对象生成任何事件。如果随后卸载文件系统,则随后将为目录及其包含的对象生成事件。
如上所述,由 rename(2) 生成的 IN_MOVED_FROM 和 IN_MOVED_TO 事件对可以通过它们的共享 cookie 值进行匹配。然而,匹配的任务有一些挑战。
在从 inotify 文件描述符读取时,这两个事件在可用的事件流中通常是连续的。但是,这不能保证。如果多个进程正在触发受监视对象的事件,那么(在极少数情况下)IN_MOVED_FROM 和 IN_MOVED_TO 事件之间可能会出现任意数量的其他事件。此外,不能保证事件对被原子地插入到队列中:可能经过一个短暂的间隔 IN_MOVED_FROM 就出现了,但 IN_MOVED_TO 没有出现。
因此,匹配由 rename(2) 生成的 IN_MOVED_FROM 和 IN_MOVED_TO 事件对本质上是活泼的。(不要忘记,如果一个对象在受监视目录之外被重命名,甚至可能没有 IN_MOVED_TO 事件。)在大多数情况下,启发式方法(例如,假设事件总是连续的)可用于确保匹配,但不可避免地会错过某些情况,导致应用程序将 IN_MOVED_FROM 和 IN_MOVED_TO 事件视为不相关。如果监视描述符因此被销毁并重新创建,那么这些监视描述符将与任何未决事件中的监视描述符不一致。(重新创建 inotify 文件描述符并重建缓存可能有助于处理这种情况。)
应用程序还应该允许 IN_MOVED_FROM 事件是可以放入当前调用 read(2) 返回的缓冲区中的最后一个事件,并且可能在下一次 read(2) 时获取随附的 IN_MOVED_TO 事件,这应该使用(小的)超时来完成,以允许插入 IN_MOVED_FROM-IN_MOVED_TO 事件对不是原子的,并且也可能没有任何 IN_MOVED_TO 事件。
在 Linux 3.19 之前,fallocate(2) 没有创建任何 inotify 事件。 从 Linux 3.19 开始,对 fallocate(2) 的调用会生成 IN_MODIFY 事件。
在 2.6.16 之前的内核中,IN_ONESHOT 掩码标志不起作用。
按照最初的设计和实现,当在一个事件之后丢弃一个监视时,IN_ONESHOT 标记不导致生成一个 IN_IGNORED 事件。但是,由于其他更改的意外影响,从 Linux 2.6.36 开始,在这种情况下会生成 IN_IGNORED 事件。
在内核 2.6.25 之前,旨在合并连续相同事件的内核代码(即,如果尚未读取较旧的事件,则可能合并两个最近的事件)代替了检查最近的事件是否可以与最旧的未读事件。
当通过调用 inotify_rm_watch(2) 删除监视描述符(或因为删除监视的文件或卸载包含它的文件系统)时,该监视描述符的任何未决未读事件仍然可供读取。由于监视描述符是随后使用 inotify_add_watch(2) 分配的,因此内核在可能的监视描述符(0 到 INT_MAX)的范围内逐步循环。在分配空闲监视描述符时,不会检查该监视描述符编号是否在 inotify 队列中有任何未决的未读事件。因此,即使一个监视描述符号的前一个化身存在未决未读事件,也可能会重新分配该监视描述符,结果应用程序可能会读取这些事件,并将它们解释为属于与新的回收的监视描述符关联的文件。实际上,遇到此错误的可能性可能非常低,因为它要求应用程序循环遍历INT_MAX 个监视描述符,释放监视描述符,同时将该监视描述符的未读事件留在队列中,然后回收该监视描述符。出于这个原因,并且由于没有关于实际应用程序中出现该错误的报告,截至 Linux 3.15,尚未进行内核更改以消除此可能的错误。
下面的程序演示了 inotify API 的用法。它标记作为命令行参数传递的目录并等待 IN_OPEN、IN_CLOSE_NOWRITE 和 IN_CLOSE_WRITE 类型的事件。
程序源码:
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.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;
int i;
ssize_t len;
char *ptr;
/* Loop while events can be read from inotify file descriptor. */
for (;;) {
/* Read some events. */
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 (ptr = buf; ptr < buf + len;
ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
/* Print event type */
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 (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);
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 */
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 */
nfds = 2;
/* Console input */
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
/* Inotify input */
fds[1].fd = fd;
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);
}
示例输出:
$ ./a.out /tmp /home/user/temp
Press enter key to terminate.
Listening for events.
IN_OPEN: /home/user/temp/foo [file]
IN_CLOSE_WRITE: /home/user/temp/foo [file]
IN_OPEN: /tmp/ [directory]
IN_CLOSE_NOWRITE: /tmp/ [directory]
Listening for events stopped.
该输出是在编辑文件 /home/user/temp/foo
和列出目录 /tmp
时记录的。文件和目录被打开前,IN_OPEN 事件发生。文件被关闭后,发生了一个 IN_CLOSE_WRITE 事件。目录被关闭后,发生了一个 IN_CLOSE_NOWRITE 事件。当用户按下 ENTER 键时程序执行结束。