inotify

Posted by Underdog Linux on December 29, 2023

原文地址:https://www.linuxjournal.com/article/8478?page=0,0

引子

John McCutchan 和我在 inotify 上工作了大约一年,最终它被合并到 Linus 的内核树中,并随内核版本 2.6.13 一起发布。 尽管这是一场漫长的斗争,但努力最终取得了成功,并且最终值得每一次重写、错误和辩论。

What Is inotify?

inotify 是一个文件更改通知系统(file change notification system),这是一项内核功能,允许应用程序请求根据事件列表监视一组文件。 当事件发生时,应用程序会收到通知。 为了有用,这样的功能必须易于使用、轻量级、开销小且灵活。 添加新检测应该很容易,并且可以轻松接收事件通知。

可以肯定的是,inotify 并不是同类产品中的第一个。 每个现代操作系统都提供某种文件通知系统; 许多网络和桌面应用程序都需要这样的功能——Linux 也是如此。 多年来,Linux 一直提供 dnotify。 问题是,dnotify 不是很好。 事实上,它很烂。

dnotify的缺点

dnotify 表面上代表目录通知(directory notify),但从未被认为易于使用。 dnotify 拥有繁琐的界面和一些令人痛苦的功能,使生活变得艰难,无法满足现代桌面的需求,在现代桌面中,事件的异步通知和信息的自由流动正在迅速成为常态。 dnotify 特别存在几个问题:

  • dnotify 只能监视目录。
  • dnotify 需要维护用户想要监测的目录的打开文件描述符。 首先,这个打开的文件描述符固定目录,不允许卸载它所在的设备。 其次,监测大量目录需要太多打开的文件描述符。
  • dnotify 与用户空间的接口是信号。 是的,说真的,信号!
  • dnotify 忽略硬链接问题。

因此,目标是双重的:设计一流的文件通知系统并确保解决 dnotify 的所有缺陷。

inotify 是一个基于 inode 的文件通知系统,不需要打开文件即可查看它。 inotify 不固定文件系统挂载——事实上,它有一个巧妙的事件,每当文件的支持文件系统被卸载时就会通知用户。 inotify 能够监视任何文件系统对象,并且在监视目录时,它能够告诉用户目录中发生更改的文件的名称。 dnotify 只能报告某些更改,要求应用程序维护 stat() 结果的内存缓存并比较任何更改。

最后,inotify 设计了一个用户空间应用程序开发人员想要使用、享受使用并从中受益的界面。 inotify 通过单个文件描述符与应用程序通信,而不是信号。 该文件描述符是 select、poll、epoll 且可读的。 简单而快速——世界是快乐的。

Getting Started with inotify

inotify 在内核 2.6.13-rc3 及更高版本中可用。 由于在该版本发布后发现并随后修复了一些错误,因此建议使用内核 2.6.13 或更高版本。 inotify 系统调用是新的系统调用,您的系统版本的 C 库可能尚不支持,在这种情况下,在线资源中列出的头文件将提供必要的 C 声明和系统调用存根 。

如果您的 C 库支持 inotify,那么您需要做的就是:

#include <sys/inotify.h>

如果没有,请获取两个头文件,将它们粘贴到与源文件相同的目录中,然后使用以下命令:

#include "inotify.h"
#include "inotify-syscalls.h"

以下示例直接使用 C 语言编写。您可以像任何其他 C 应用程序一样编译它们。

Initialize, inotify!

inotify 通过 inotify_init() 系统调用进行初始化,该调用在内核中实例化 inotify 实例并返回关联的文件描述符:

int inotify_init (void);

失败时,inotify_init() 返回-1并根据需要设置 errno。 最常见的 errno 值是 EMFILE 和 ENFILE,它们分别表示已达到每个用户和系统范围的打开文件限制。 Usage is simple:

int fd;

fd = inotify_init ();
if (fd < 0)
        perror ("inotify_init");

监测

inotify 的核心是监视,它由指定要监视的内容的路径名和指定要监视的事件掩码组成。 inotify 可以监视许多不同的事件:打开、关闭、读取、写入、创建、删除、移动、元数据更改和卸载。 每个 inotify 实例可以有数千个监视,每个监视不同的事件列表。

增加监测

监视是通过 inotify_add_watch() 系统调用添加的:

int inotify_add_watch (int fd, const char *path, __u32 mask);

对 inotify_add_watch() 的调用将监视由文件路径上的位掩码指定的一个或多个事件添加到与文件描述符 fd 关联的 inotify 实例。 成功后,调用将返回一个监视描述符,用于唯一标识此特定监视。 失败时,返回-1,并根据需要设置 errno。

int wd;

wd = inotify_add_watch (fd,
                "/home/rlove/Desktop",
                IN_MODIFY | IN_CREATE | IN_DELETE);

if (wd < 0)
        perror ("inotify_add_watch");

此示例在目录 /home/rlove/Desktop 上添加监视以监视任何修改、文件创建或文件删除。

表 1 显示了有效事件。

Event Description
IN_ACCESS 文件已读取
IN_MODIFY 文件已写入
IN_ATTRIB 文件的元数据(inode 或 xattr)已更改
IN_CLOSE_WRITE 文件已关闭(并且已打开以供写入)
IN_CLOSE_NOWRITE 文件已关闭(并且未打开以进行写入)
IN_OPEN 文件已打开
IN_MOVED_FROM 文件从监控列表中移除
IN_MOVED_TO 文件加入监控列表
IN_DELETE 文件被删除
IN_DELETE_SELF 监控本身被删除

表 2 显示了提供的辅助事件。

Event Description  
IN_CLOSE IN_CLOSE_WRITE IN_CLOSE_NOWRITE
IN_MOVE IN_MOVED_FROM IN_MOVED_TO
IN_ALL_EVENTS 所有事件的按位或  

例如,如果应用程序想要知道何时打开或关闭文件 safe_combination.txt,它可以执行以下操作:

int wd;

wd = inotify_add_watch (fd,
                "safe_combination.txt",
                IN_OPEN | IN_CLOSE);

if (wd < 0)
        perror ("inotify_add_watch");

Receiving Events

初始化 inotify 并添加监视后,您的应用程序现在已准备好接收事件。 事件在事件发生时实时异步排队,但通过 read() 系统调用同步读取。 该调用将阻塞,直到事件准备就绪,然后在任何事件排队后返回所有可用事件。

事件以 inotify_event 结构的形式传递,其定义为:

struct inotify_event {
        __s32 wd;             /* watch descriptor */
        __u32 mask;           /* watch mask */
        __u32 cookie;         /* cookie to synchronize two events */
        __u32 len;            /* length (including nulls) of name */
        char name[0];        /* stub for possible name */
};
  • wd wd 字段是最初由 inotify_add_watch() 返回的监视描述符。 应用程序负责将此标识符映射回文件名。

  • mask mask字段是表示发生的事件的位掩码。

  • cookie cookie 字段是一个唯一标识符,将两个相关但独立的事件链接在一起。 它用于将 IN_MOVED_FROM 和 IN_MOVED_TO 事件链接在一起。 我们稍后再看。

  • len len 字段是name 字段的长度,如果该事件没有name ,则该字段不为零。 长度包含任何潜在的填充,即 name 字段上 strlen() 的结果可能小于 len。

  • name name 字段包含事件发生的对象的名称,相对于 wd(如果适用)。 例如,如果 /etc 中写入的监视触发写入 /etc/vimrc 的事件,则 name 字段将包含 vimrc,并且 wd 字段将链接回 /etc 监视。 相反,如果监视文件 /etc/fstab 进行读取,则触发的读取事件的 len 值将为零且没有任何关联名称,因为监视描述符直接与受影响的文件关联。

名称的大小是动态的。

参阅零长数组的概念

如果事件没有关联的文件名,则根本不会发送任何名称,并且不会消耗任何空间。 如果事件确实有关联的文件名,则名称字段将动态分配并跟踪结构 len 字节。 这种方法允许名称的长度大小不同,并且在不需要时不占用空间。

因为名称字段是动态的,所以传递给 read() 的缓冲区的大小是未知的。 如果大小太小,系统调用将返回零,警告应用程序。 然而,inotify 允许用户空间同时“吸收”多个事件。 因此,大多数应用程序应该传入一个大的缓冲区,inotify 将填充尽可能多的事件。

听起来很复杂,但用法很简单:

/* size of the event structure, not counting name */
#define EVENT_SIZE  (sizeof (struct inotify_event))

/* reasonable guess as to size of 1024 events */
#define BUF_LEN        (1024 * (EVENT_SIZE + 16)

char buf[BUF_LEN];
int len, i = 0;

len = read (fd, buf, BUF_LEN);
if (len < 0) {
        if (errno == EINTR)
                /* need to reissue system call */
        else
                perror ("read");
} else if (!len)
        /* BUF_LEN too small? */

while (i < len) {
        struct inotify_event *event;

        event = (struct inotify_event *) &buf[i];

        printf ("wd=%d mask=%u cookie=%u len=%u\n",
                event->wd, event->mask,
                event->cookie, event->len);

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

        i += EVENT_SIZE + event->len;
}

采用这种方法是为了允许一次性读取和处理许多事件,并处理动态大小的名称。 聪明的读者会立即质疑以下代码对于对齐要求是否安全:

while (i < len) {
        struct inotify_event *event;
        event = (struct inotify_event *) &buf[i];

        /* ... */

        i += EVENT_SIZE + event->len;
}

它的确是。 这就是 len 字段可能比字符串长度长的原因。 字符串后面可能会出现其他空字符,将其填充到确保后续结构正确对齐的大小。

但我不想读取!

必须在 read() 系统调用上阻塞听起来不太吸引人,除非您的应用程序是线程密集型的,在这种情况下,嘿,只需多一个线程! 值得庆幸的是,inotify 文件描述符可以被轮询或选择,从而允许 inotify 与其他 I/O 一起复用,并可选择集成到应用程序的主循环中。

struct timeval time;
fd_set rfds;
int ret;

/* timeout after five seconds */
time.tv_sec = 5;
time.tv_usec = 0;

/* zero-out the fd_set */
FD_ZERO (&rfds);

/*
 * add the inotify fd to the fd_set -- of course,
 * your application will probably want to add
 * other file descriptors here, too
 */
FD_SET (fd, &rfds);

ret = select (fd + 1, &rfds, NULL, NULL, &time);
if (ret < 0)
        perror ("select");
else if (!ret)
        /* timed out! */
else if (FD_ISSET (fd, &rfds)
        /* inotify events are available! */

您可以使用 pselect()、poll() 或 epoll() 遵循类似的方法 — 选择您自己的选择。

Events

inotify_event 结构中的 mask 字段描述了发生的事件。 除了前面列出的事件之外,表 3 还显示了发送的事件(如果适用)。

表 3. 涉及一般变更的事件

Name Description
IN_UNMOUNT 支持文件系统已卸载
IN_Q_OVERFLOW inotify 队列溢出
IN_IGNORED 由于文件被删除或其文件系统被卸载,监测被自动删除

此外,IN_ISDIR 位被设置,告诉应用程序事件是否针对目录发生。 这不仅仅是一种方便——考虑一下已删除文件的情况。

由于位掩码中存在诸如 IN_ISDIR 之类的标志,因此永远不应将其直接与可能的事件进行比较。 相反,应该单独测试这些位。 例如:

if (event->mask & IN_DELETE) {
        if (event->mask & IN_ISDIR)
                printf ("Directory deleted!\n");
        else
                printf ("File deleted!\n");
}

Modifying Watches

通过使用更新的事件掩码调用 inotify_add_watch() 来修改监视。 如果监视已经存在,则只需更新掩码并返回原始监视描述符。

Removing Watches

通过 inotify_rm_watch() 系统调用删除监视:

int inotify_rm_watch (int fd, int wd);

对 inotify_rm_watch() 的调用将从与文件描述符 fd 关联的 inotify 实例中删除与监视描述符 wd 关联的监视。 该调用在成功时返回零,在失败时返回-1,在这种情况下,会根据需要设置 errno。

int ret;

ret = inotify_rm_watch (fd, wd);
if (ret)
        perror ("inotify_rm_watch");

Shutting inotify Down

如果在添加监视时将 IN_ONESHOT 值与事件掩码进行“或”运算,则在生成第一个事件期间会自动删除监视。 在重新添加监视之前,不会针对该文件生成后续事件。 某些应用程序需要这种行为,例如 Samba,其中一次性支持模仿 Microsoft Windows 上的文件更改通知系统的行为。

int wd;

wd = inotify_add_watch (fd,
                "/home/rlove/Desktop",
                IN_MODIFY | IN_ONESHOT);

if (wd < 0)
        perror ("inotify_add_watch");

On Unmount

dnotify 的最大问题之一(除了信号和基本上其他所有内容)是目录上的 dnotify 监视要求该目录保持打开状态。 因此,监视 USB 钥匙串驱动器上的目录会阻止驱动器卸载。

inotify 通过不要求打开任何文件来解决这个问题。 不过,inotify 更进一步,当文件所在的文件系统被卸载时,它会发出 IN_UNMOUNT 事件。 它还会自动销毁监测并进行清理。

Moves

移动事件很复杂,因为 inotify 可能正在监视文件移入或移出的目录,而不是其他目录。 因此,并不总是能够提醒用户移动中涉及的文件的源和目标。 仅当应用程序正在监视这两个目录时,inotify 才能向应用程序发出警报。

在这种情况下,inotify 从源目录的监视描述符发出 IN_MOVED_FROM,并从目标目录的监视描述符发出 IN_MOVED_TO。 如果仅观看其中之一,则仅发送一个事件。

获取队列的大小

待处理事件队列的大小可以通过FIONREAD获取:

unsigned int queue_len;
int ret;

ret = ioctl (fd, FIONREAD, &queue_len);
if (ret < 0)
        perror ("ioctl");
else
        printf ("%u bytes pending in queue\n", queue_len);

这对于实现限制很有用:仅当事件数量变得足够大时才从队列中读取。

Configuring inotify

inotify 可以通过 procfs 和 sysctl 进行配置。

/proc/sys/filesystem/inotify/max_queued_events 是一次可以排队的最大事件数。 如果队列达到此大小,新事件将被丢弃,但始终会发送 IN_Q_OVERFLOW 事件。 由于队列非常大,即使观察许多对象,溢出也很少见。 默认值为每个队列 16,384 个事件。

/proc/sys/filesystem/inotify/max_user_instances 是给定用户可以实例化的 inotify 实例的最大数量。 默认值为每个用户 128 个实例。

/proc/sys/filesystem/inotify/max_user_watches 是每个实例的最大监视数。 每个实例的默认值为 8,192 个监视。

这些旋钮的存在是因为内核内存是宝贵的资源。 尽管任何用户都可以读取这些文件,但只有系统管理员可以写入它们。

结论

inotify 是一个简单但功能强大的文件更改通知系统,具有直观的用户界面、出色的性能、支持许多不同的事件和众多功能。 inotify 目前在多个项目中使用,包括 Beagle(高级桌面索引系统)和 Gamin(FAM 替代品)。

接下来什么应用程序将使用inotify?

Resources for this article: https://www.linuxjournal.com/article/8534