笔记和练习博客总目录见开始读TLPI。一些应用程序需要能够监视文件或目录以确定所监视对象是否发生了事件。例如图形文件管理器需要能够确定当前显示的目录中何时添加或删除文件或者守护进程可能希望监视其配置文件以便知道文件是否已更改。从内核 2.6.13 开始Linux 提供了 inotify 机制它允许应用程序监视文件事件。本章描述了 inotify 的使用。inotify 机制取代了较早的 dnotify 机制后者提供了 inotify 功能的子集。本章末尾将简要介绍 dnotify重点说明为什么 inotify 更好。inotify 和 dnotify 机制是Linux 特有的。其他一些系统也提供类似的机制。例如BSD 系统提供 kqueue API。有一些库提供的 API 比 inotify 和 dnotify 更抽象、更可移植。对于某些应用程序使用这些库可能更可取。这些库中的一些在系统可用的情况下会使用 inotify 或 dnotify。两个这样的库是 FAM文件更改监视器http://oss.sgi.com/projects/fam/和 Gaminhttp://www.gnome.org/~veillard/gamin/。 本章实际就是介绍inotify API详见inotify(7)。19.1 Overview使用 inotify API 的关键步骤如下应用程序使用 inotify_init() 创建一个 inotify 实例。该系统调用返回一个文件描述符该描述符用于在后续操作中引用该 inotify 实例。应用程序通过使用 inotify_add_watch() 将项目添加到前一步创建的 inotify 实例的监视列表中从而通知内核哪些文件是感兴趣的。每个监视项目由一个路径名和一个相关的位掩码组成。位掩码指定要监控该路径名的一组事件。作为其函数结果inotify_add_watch() 返回一个监视描述符该描述符用于在后续操作中引用该监视。(inotify_rm_watch() 系统调用执行相反的任务移除之前添加到 inotify 实例的监视。)为了获取事件通知应用程序对 inotify 文件描述符执行 read() 操作。每次成功的 read() 会返回一个或多个 inotify_event 结构每个结构包含通过此 inotify 实例监视的某个路径名上发生的事件信息。当应用程序完成监控时它会关闭 inotify 文件描述符。这会自动移除与该 inotify 实例关联的所有监视项目。inotify 机制可以用于监视文件或目录。当监视一个目录时应用程序会被告知该目录本身以及目录内文件的事件。inotify 监控机制不是递归的。如果应用程序想监视整个目录子树中的事件则必须对树中的每个目录发出 inotify_add_watch() 调用。inotify 文件描述符可以使用 select()、poll、epoll 以及自 Linux 2.6.25 起的信号驱动 I/O 进行监控。如果有可读取的事件这些接口会将 inotify 文件描述符标记为可读。有关这些接口的更多详细信息请参见第 63 章。inotify 机制是一个可选的 Linux 内核组件可通过选项 CONFIG_INOTIFY 和 CONFIG_INOTIFY_USER 配置。19.2 The inotify APIinotify_init() 系统调用创建一个新的 inotify 实例。#includesys/inotify.hintinotify_init(void);作为其函数结果inotify_init() 返回一个文件描述符。这个文件描述符是用于在后续操作中引用 inotify 实例的句柄。从内核 2.6.27 开始Linux 支持一个新的非标准系统调用 inotify_init1()。该系统调用执行与 inotify_init() 相同的任务但提供了一个额外的参数 flags可用于修改系统调用的行为。支持两个标志。IN_CLOEXEC 标志使内核为新的文件描述符启用关闭执行标志FD_CLOEXEC。由于与第 4.3.1 节中描述的 open() O_CLOEXEC 标志的原因相同此标志非常有用。IN_NONBLOCK 标志使内核在底层打开的文件描述符上启用 O_NONBLOCK 标志这样将来的读取操作将是非阻塞的。这节省了额外调用 fcntl() 来实现相同的结果。inotify_add_watch() 系统调用要么向由文件描述符 fd 引用的 inotify 实例的监视列表中添加一个新的监视项要么修改现有的监视项。参见图 19-1。#includesys/inotify.hintinotify_add_watch(intfd,constchar*pathname,uint32_tmask);Figure 19-1: An inotify instance and associated kernel data structurespathname 参数用于标识要创建或修改监视项的文件。调用者必须对该文件具有读取权限。文件权限检查只在调用 inotify_add_watch() 时执行一次。只要监视项继续存在即使文件权限后来被更改导致调用者不再具有读取权限调用者仍将继续接收文件通知。mask 参数是一个位掩码用于指定要监视 pathname 的事件。稍后我们将详细说明 mask 中可以指定的位值。如果 pathname 之前未被添加到 fd 的监视列表中则 inotify_add_watch() 会在列表中创建一个新的监视项并返回一个新的非负监视描述符该描述符用于在后续操作中引用该监视项。此监视描述符在此 inotify 实例中是唯一的。如果路径名之前已被添加到 fd 的监视列表中那么 inotify_add_watch() 会修改该路径名的现有监视项的掩码并返回该项的监视描述符。该监视描述符将与最初将路径名添加到该监视列表的 inotify_add_watch() 调用返回的描述符相同。我们将在下一节描述 IN_MASK_ADD 标志时进一步说明掩码可能如何被修改。inotify_rm_watch() 系统调用从由文件描述符 fd 指定的 inotify 实例中移除由 wd 指定的监视项。#includesys/inotify.hintinotify_rm_watch(intfd,uint32_twd);wd 参数是由之前对 inotify_add_watch() 的调用返回的一个观察描述符。uint32_t 数据类型是一个无符号 32 位整数。移除一个监视会导致为该监视描述符生成一个 IN_IGNORED 事件。我们稍后会更多地讨论这个事件。19.3 inotify Events当我们使用 inotify_add_watch() 创建或修改一个监视器时mask 位掩码参数用于指定要监视给定路径名的事件。可以在 mask 中指定的事件位由表 19-1 的 In 列指出。Table 19-1: inotify eventsBit valueInOutDescriptionIN_ACCESS●●File was accessed (read())IN_ATTRIB●●File metadata changedIN_CLOSE_WRITE●●File opened for writing was closedIN_CLOSE_NOWRITE●●File opened read-only was closedIN_CREATE●●File/directory created inside watched directoryIN_DELETE●●File/directory deleted from within watched directoryIN_DELETE_SELF●●Watched file/directory was itself deletedIN_MODIFY●●File was modifiedIN_MOVE_SELF●●Watched file/directory was itself movedIN_MOVED_FROM●●File moved out of watched directoryIN_MOVED_TO●●File moved into watched directoryIN_OPEN●●File was openedIN_ALL_EVENTS●Shorthand for all of the above input eventsIN_MOVE●Shorthand for IN_MOVED_FROM | IN_MOVED_TOIN_CLOSE●Shorthand for IN_CLOSE_WRITE | IN_CLOSE_NOWRITEIN_DONT_FOLLOW●Don’t dereference symbolic link (since Linux 2.6.15)IN_MASK_ADD●Add events to current watch mask for pathnameIN_ONESHOT●Monitor pathname for just one eventIN_ONLYDIR●Fail if pathname is not a directory (since Linux 2.6.15)IN_IGNORED●Watch was removed by application or by kernelIN_ISDIR●Filename returned in name is a directoryIN_Q_OVERFLOW●Overflow on event queueIN_UNMOUNT●File system containing object was unmounted表19-1中大多数位的含义从其名称可以看出。以下列表澄清了一些细节当文件的元数据如权限、所有者、链接计数、扩展属性、用户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 指定如果路径名是符号链接则不应解引用它。这允许应用程序监视符号链接本身而不是它所指向的文件。如果我们执行一个 inotify_add_watch() 调用并指定一个已经通过此 inotify 文件描述符被监视的路径名那么默认情况下给定的掩码将用于替换该监视项的当前掩码。如果指定了 IN_MASK_ADD则当前掩码会通过与 mask 中给定的值进行或操作来修改。IN_ONESHOT 允许应用程序对路径名进行一次事件监视。该事件发生后监视项会自动从监视列表中移除。IN_ONLYDIR 允许应用程序仅在路径名是目录时进行监控。如果路径名不是目录则 inotify_add_watch() 会失败并返回错误 ENOTDIR。使用此标志可以防止可能出现的竞争条件这些条件可能会在我们想要确保监控的是目录时发生。19.4 Reading inotify Events在监视列表中注册了项目后应用程序可以使用 read() 从 inotify 文件描述符读取事件来确定哪些事件已经发生。如果到目前为止没有事件发生则 read() 会阻塞直到发生事件除非文件描述符已设置 O_NONBLOCK 状态标志在这种情况下如果没有可用事件read() 会立即失败并返回错误 EAGAIN。事件发生后每次 read() 会返回一个缓冲区见图 19-2其中包含一个或多个以下类型的结构 此结构的描述见inotify(7)。structinotify_event{intwd;/* Watch descriptor on which event occurred */uint32_tmask;/* Bits describing event that occurred */uint32_tcookie;/* Cookie for related events (for rename()) */uint32_tlen;/* Size of name field */charname[];/* Optional null-terminated filename */};图略Figure 19-2: An input buffer containing three inotify_event structureswd 字段告诉我们此事件发生的监视描述符。此字段包含先前调用 inotify_add_watch() 返回的一个值。当应用程序通过相同的 inotify 文件描述符监视多个文件或目录时wd 字段非常有用。它提供了一个链接使应用程序能够确定事件发生的特定文件或目录。为此应用程序必须维护一个账目数据结构将监视描述符与路径名关联起来。mask 字段返回描述事件的位掩码。mask 中可能出现的位的范围在表 19-1 的输出列中指出。注意以下关于特定位的其他细节当监视被移除时会生成一个 IN_IGNORED 事件。这可能有两个原因应用程序使用 inotify_rm_watch() 调用显式移除监视或者监视被内核隐式移除因为被监视的对象被删除或其所在的文件系统已被卸载。当使用 IN_ONESHOT 建立的监视因触发事件而自动移除时不会生成 IN_IGNORED 事件。如果事件的主体是一个目录那么除了其他一些标志外mask 中还会设置 IN_ISDIR 位。IN_UNMOUNT 事件通知应用程序包含被监视对象的文件系统已被卸载。在此事件之后会发送包含 IN_IGNORED 位的后续事件。我们在 19.5 节中描述了 IN_Q_OVERFLOW该节讨论了排队 inotify 事件的限制。cookie 字段用于将相关事件关联在一起。目前该字段仅在文件重命名时使用。当发生这种情况时会为文件被重命名的目录生成一个 IN_MOVED_FROM 事件然后会为文件被重命名到的目录生成一个 IN_MOVED_TO 事件。如果文件在同一目录中被改名则两个事件都发生在同一目录。这两个事件的 cookie 字段将具有相同的唯一值从而允许应用程序将它们关联起来。当监控目录中的文件发生事件时name 字段用于返回一个以空字符结尾的字符串用于标识该文件。如果事件发生在被监控的对象本身则 name 字段未使用并且 len 字段将包含 0。len 字段指示为 name 字段实际分配了多少字节。该字段是必要的因为在 name 中存储的字符串结束和 read() 返回的缓冲区中下一个 inotify_event 结构的起始之间可能存在额外的填充字节参见图 19-2。单个 inotify 事件的长度因此为 sizeof(struct inotify_event) len。如果传递给 read() 的缓冲区太小无法容纳下一个 inotify_event 结构则 read() 会以 EINVAL 错误失败以提醒应用程序这一事实。在 2.6.21 之前的内核中read() 在这种情况下会返回 0。改为使用 EINVAL 错误可以更清楚地表明发生了编程错误。应用程序可以通过使用更大的缓冲区再次调用 read() 来应对这种情况。然而通过确保缓冲区始终足够大以容纳至少一个事件可以完全避免这个问题传递给 read() 的缓冲区应至少为 (sizeof(struct inotify_event) NAME_MAX 1) 字节其中 NAME_MAX 是文件名的最大长度加 1 用于终止空字节。使用比最小值更大的缓冲区大小可以让应用程序高效地通过一次 read() 检索多个事件。从 inotify 文件描述符进行的 read() 返回可用事件数量和可以放入提供的缓冲区中的事件数量中的最小值。调用 ioctl(fd, FIONREAD, numbytes) 返回当前可以从由文件描述符 fd 指向的 inotify 实例中读取的字节数。从 inotify 文件描述符读取的事件形成有序队列。因此例如可以保证当文件被重命名时IN_MOVED_FROM 事件会在 IN_MOVED_TO 事件之前被读取。当向事件队列的末尾添加新事件时如果两个事件在 wd、mask、cookie 和 name 上的值相同内核会将该事件与队列尾的事件合并因此新事件实际上不会被排队。这样做是因为许多应用程序不需要知道同一事件的重复实例而丢弃多余的事件可以减少事件队列所需的内核内存量。然而这也意味着我们不能使用 inotify 来可靠地确定重复事件发生的次数或频率。Example program尽管前面的描述中有很多细节但 inotify API 实际上非常容易使用。清单 19-1 演示了 inotify 的使用。Listing 19-1: Using the inotify API// inotify/demo_inotify.c// 代码略清单19-1中的程序执行以下步骤使用 inotify_init() 创建一个 inotify 文件描述符 。使用 inotify_add_watch() 为程序命令行参数中指定的每个文件添加一个监视项 。每个监视项监视所有可能的事件。执行一个无限循环从 inotify 文件描述符中读取一缓冲区的事件。调用 displayInotifyEvent() 函数显示该缓冲区中每个 inotify_event 结构的内容。以下的 shell 会话演示了清单 19-1 中程序的使用。我们启动了一个在后台运行的程序实例用于监控两个目录$mkdirdir1 dir2 $ ./demo_inotify dir1 dir2[1]16168Watching dir1 using wd1Watching dir2 using wd2然后我们执行在两个目录中生成事件的命令。我们首先使用 cat(1) 创建一个文件$catdir1/aaa Read32bytes from inotify fd wd1;maskIN_CREATE nameaaa Read32bytes from inotify fd wd1;maskIN_OPEN nameaaa上述由后台程序生成的输出显示 read() 获取了一个包含两个事件的缓冲区。我们接着为文件输入一些内容然后输入终端的文件结束字符输入Hello world Read32bytes from inotify fd wd1;maskIN_MODIFY nameaaa 输入Ctrl-D Read32bytes from inotify fd wd1;maskIN_CLOSE_WRITE nameaaa然后我们将文件重命名到另一个受监控的目录。这会产生两个事件一个是文件移动的源目录观察描述符 1另一个是目标目录观察描述符 2$mvdir1/aaa dir2/bbb Read32bytes from inotify fd wd1;cookie1275;maskIN_MOVED_FROM nameaaa Read32bytes from inotify fd wd2;cookie1275;maskIN_MOVED_TO namebbb这两个事件共享相同的 cookie 值允许应用程序将它们关联起来。当我们在其中一个被监视的目录下创建一个子目录时生成的事件中的掩码包含 IN_ISDIR 位表明事件的对象是一个目录$mkdirdir2/ddd Read32bytes from inotify fd wd2;maskIN_CREATE IN_ISDIR nameddd在这一点上值得重复一下inotify 监控不是递归的。如果应用程序想要监控新创建的子目录中的事件那么它需要进一步调用 inotify_add_watch() 并指定子目录的路径名。最后我们移除其中一个被监控的目录$rmdirdir1 Read16bytes from inotify fd wd1;maskIN_DELETE_SELF Read16bytes from inotify fd wd1;maskIN_IGNORED最后一个事件IN_IGNORED是用于通知应用程序内核已将此监视项从监视列表中移除而生成的。19.5 Queue Limits and /proc Files对 inotify 事件进行排队需要内核内存。因此内核会对 inotify 机制的运行施加各种限制。超级用户可以通过 /proc/sys/fs/inotify 目录下的三个文件来配置这些限制max_queued_events当调用 inotify_init() 时该值用于设置新 inotify 实例中可以排队的事件数量的上限。如果达到此上限将生成 IN_Q_OVERFLOW 事件多余的事件将被丢弃。溢出事件的 wd 字段值将为 -1。max_user_instances这是对每个实际用户 ID 可以创建的 inotify 实例数量的限制。max_user_watches这是对每个实际用户 ID 可以创建的监视项数量的限制。这三个文件的典型默认值分别为 16,384、128 和 8192。$cd/proc/sys/fs/inotify $lsmax_queued_events max_user_instances max_user_watches $cat*163841282448819.6 An Older System for Monitoring File Events: dnotifyLinux 提供了另一种监控文件事件的机制。这种机制被称为 dnotify自内核 2.4 以来就已经存在但已经被 inotify 所取代。与 inotify 相比dnotify 机制存在一些限制dnotify 机制通过向应用程序发送信号来提供事件通知。使用信号作为通知机制会使应用程序设计变得复杂第 22.12 节。它也使得在库中使用 dnotify 变得困难因为调用程序可能会改变通知信号的处理方式。而 inotify 机制不使用信号。dnotify 的监控单位是目录。当某个文件在该目录中进行操作时应用程序将收到通知。相比之下inotify 可以用于监控目录或单个文件。为了监控一个目录dnotify 需要应用程序为该目录打开一个文件描述符。使用文件描述符会产生两个问题。首先因为文件系统被占用该目录所在的文件系统无法卸载。其次由于每个目录都需要一个文件描述符应用程序可能会消耗大量的文件描述符。由于 inotify 不使用文件描述符因此可以避免这些问题。dnotify 提供的关于文件事件的信息不如 inotify 精确。当一个文件在被监控的目录中被修改时dnotify 会告诉我们发生了事件但不会告诉我们涉及事件的是哪个文件。应用程序必须通过缓存目录内容的信息来确定这一点。此外inotify 提供比 dnotify 更详细的关于事件类型的信息。在某些情况下dnotify 无法可靠地提供文件事件的通知。关于 dnotify 的更多信息可以在 fcntl(2) 手册页中 F_NOTIFY 操作的描述以及内核源码文件 Documentation/dnotify.txt 中找到。19.7 SummaryLinux 特有的 inotify 机制允许应用程序在监控的一组文件和目录发生事件文件被打开、关闭、创建、删除、修改、重命名等时获得通知。inotify 机制取代了较早的 dnotify 机制。19.8 Exercise参见TLPI 第19 章 练习Monitoring File Events。