Kernel Korner - inotify 简介

作者:Robert Love

当 inotify 最终合并到 Linus 的内核树并在内核版本 2.6.13 中发布时,John McCutchan 和我已经为此工作了大约一年。虽然经历了一番漫长的挣扎,但这项努力最终获得了成功,并且最终值得每一次重写、错误和辩论。

什么是 inotify?

inotify 是一个文件更改通知系统——一个内核特性,允许应用程序请求监视一组文件,以应对一系列事件。当事件发生时,应用程序会收到通知。为了有用,这样的特性必须易于使用、轻量级、开销小且灵活。它应该易于添加新的监视,并且轻松地接收事件通知。

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

dnotify,表面上代表目录通知(directory notify),从未被认为是易于使用的。dnotify 具有笨拙的接口和几个令人痛苦的功能,使得生活变得艰苦,未能满足现代桌面的需求,在现代桌面中,事件的异步通知和信息的自由流动正在迅速成为常态。特别是,dnotify 有几个问题

  • dnotify 只能监视目录。

  • dnotify 需要维护一个指向用户想要监视的目录的打开文件描述符。首先,这个打开的文件描述符会锁定目录,不允许卸载它所在的设备。其次,监视大量目录需要太多的打开文件描述符。

  • dnotify 与用户空间的接口是信号。是的,认真地说,是信号!

  • dnotify 忽略了硬链接的问题。

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

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

最后,inotify 的设计理念是提供一个用户空间应用程序开发人员想要使用、乐于使用并从中受益的接口。inotify 没有使用信号,而是通过单个文件描述符与应用程序通信。这个文件描述符是可 select、poll、epoll 和 read 的。简单而快速——世界皆大欢喜。

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 应用程序一样编译它们。

初始化 inotify!

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

int inotify_init (void);

如果失败,inotify_init() 返回负一并根据情况设置 errno。最常见的 errno 值是 EMFILE 和 ENFILE,它们分别表示达到了每个用户和系统范围的打开文件限制。

用法很简单

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 实例上的文件路径添加由位掩码 mask 给出的一个或多个事件的监视。成功时,该调用返回一个监视描述符,该描述符用于唯一标识此特定监视。失败时,返回负一并根据情况设置 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 显示了有效事件。

表 1. 有效事件

事件描述
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 显示了提供的辅助事件。

表 2. 辅助事件

事件描述
IN_CLOSEIN_CLOSE_WRITE | IN_CLOSE_NOWRITE
IN_MOVEIN_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");

接收事件

在 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 字段是由 inotify_add_watch() 最初返回的监视描述符。应用程序负责将此标识符映射回文件名。

mask 字段是一个位掩码,表示发生的事件。

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

len 字段是 name 字段的长度,如果此事件没有名称,则为非零值。长度包含任何可能的填充——也就是说,name 字段上 strlen() 的结果可能小于 len。

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

name 的大小是动态的。如果事件没有关联的文件名,则根本不发送名称,也不占用空间。如果事件确实有关联的文件名,则 name 字段是动态分配的,并在结构后附加 len 个字节。这种方法允许名称的长度大小可变,并在不需要时不占用空间。

由于 name 字段是动态的,因此传递给 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 多路复用,并可选择集成到应用程序的主循环中。

以下是使用 select() 监视 inotify 文件描述符的示例

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() 采用类似的方法——任您选择。

事件

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

表 3. 涵盖常规更改的事件

名称描述
IN_UNMOUNT后备文件系统已卸载。
IN_Q_OVERFLOWinotify 队列溢出。
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");
}

修改监视

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

删除监视

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

int inotify_rm_watch (int fd, int wd);

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

用法和往常一样简单

int ret;

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

关闭 inotify

要销毁任何现有的监视、挂起的事件以及 inotify 实例本身,请对 inotify 实例的文件描述符调用 close() 系统调用。例如

int ret;

ret = close (fd);
if (ret)
        perror ("close");

一次性支持

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

用法自然很简单

int wd;

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

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

关于卸载

dnotify 的最大问题之一(除了信号和基本上所有其他问题之外)是,对目录的 dnotify 监视要求该目录保持打开状态。因此,监视 USB 钥匙链驱动器上的目录会阻止驱动器卸载。inotify 通过不要求打开任何文件来解决此问题。

不过,inotify 更进一步,并在文件所在的文件系统被卸载时发送 IN_UNMOUNT 事件。它还会自动销毁监视并清理。

移动

移动事件很复杂,因为 inotify 可能正在监视文件移动到或移动自的目录,但并非两者都监视。因此,并非总是可以向用户警告移动中涉及的文件的源目标。只有当应用程序同时监视两个目录时,inotify 才能向应用程序警告两者。

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

为了将两个不同的移动到/从事件联系起来,inotify 将 inotify_event 结构中的 cookie 字段设置为唯一的非零值。因此,具有匹配 cookie 的两个事件是相关的,一个显示源,另一个显示移动的目标。

获取队列大小

可以通过 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);

这对于实现节流非常有用:仅当事件数量增长到足够大时才从队列中读取。

配置 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 的应用程序是什么?

本文的资源: /article/8534

Robert Love 是 Novell 的 Ximian 桌面组的高级内核黑客,也是Linux 内核开发(SAMS 2005)的作者,该书现在是第二版。他拥有佛罗里达大学的计算机科学和数学学位。Robert 住在马萨诸塞州剑桥市。

加载 Disqus 评论