Kernel Korner - inotify 简介
当 inotify 最终合并到 Linus 的内核树并在内核版本 2.6.13 中发布时,John McCutchan 和我已经为此工作了大约一年。虽然经历了一番漫长的挣扎,但这项努力最终获得了成功,并且最终值得每一次重写、错误和辩论。
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 在内核 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_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_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");
在 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 还显示了也发送的事件(如果适用)。
此外,还设置了位 IN_ISDIR,以告知应用程序事件是否针对目录发生。这不仅仅是为了方便——考虑已删除文件的情况。
由于位掩码中存在诸如 IN_ISDIR 之类的标志,因此永远不应将其与可能的事件直接比较。相反,应单独测试这些位。例如
if (event->mask & IN_DELETE) { if (event->mask & IN_ISDIR) printf ("Directory deleted!\n"); else printf ("File deleted!\n"); }
监视通过 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 实例的文件描述符调用 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 可通过 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 住在马萨诸塞州剑桥市。