inotify 是 Linux 系统提供的一种监视文件系统事件的机制,它允许应用程序监视文件或目录的变化,并在文件或目录发生变化时接收通知。这对于实时监视文件系统的变化非常有用,比如监视配置文件的更改、文件的创建、修改或删除等。
inotify API
1. inotify_init()
inotify_init() 系统调用可创建一新的 inotify 实例。
成功返回文件描述符,错误返回 -1
#include <sys/inotify.h>
int inotify_init (void) ;
int inotify_init1 (int flags) ;
Linux 自内核 2.6.27 开始支持一个新的、非标准的系统调用 inotify_init1()。该系统调所 执行的任务与 inotify_init()相同,但提供了一个额外的参数 flag,用于修改系统调用的行为。 该参数支持的标志有二:
-
IN_CLOEXEC 标志会使内核针对新文件描述符激活 close-on-exec 标志(FD_CLOEXEC)。引入该标志的原因正如 open()的 O_CLOEXEC 标志一样。
-
IN_NONBLOCK 标志会导致内核激活底层打开文件描述的 O_NONBLOCK 标志,如此一 来,未来的读操作将是非阻塞式的,省得还要额外调用 fcntl()来获得相同效果
2. inotify_add_watch
成功返回文件描述符,错误返回 -1
#include <sys/inotify.h>
int inotify_add_watch (int fd, const char * pathname, uint32_t mask);
-
针对文件描述符 fd 所指代 inotify 实例的监控列表,系统调用 inotify_add_watch()既可以追加新的监控项,也可以修改现有监控项
-
参数 pathname 标识欲创建或修改的监控项所对应的文件。调用程序必须对该文件具有读权 限(调用 inotify_add_watch()时,会对文件权限做一次性检查。只要监控项继续存在,即便有人更改了文件权限,使调用程序不再对文件具有读权限,调用程序依然会继续收到文件的通 知消息)
-
参数 mask 为一位掩码,针对 pathname 定义了意欲监控的事件
3. inotify_rm_watch
成功返回 0 ,错误返回 -1
#include <sys/inotify.h>
int inotify_rm_watch (int fd, int wd);
- fd inotify_init() 返回的文件描述符
- 参数 wd 是一监控描述符,由之前对 inotify_add_watch()的调用返回。 (uint32_t 数据类型为一无符号 32 位整数。) 删除监控项会为该监控描述符生成 IN_IGNORED 事件
4. inotify 事件
使用 inotify_add_watch()删除或修改监控项时,位掩码参数 mask 标识了针对给定路径名 (pathname)而要监控的事件,“in”列列出了可在 mask 中定义的事件位。
| 位值 | in | out | 描述 |
|---|---|---|---|
| IN_ACCESS | ● | ● | 文件被访问(read()) |
| IN_ATTRIB | ● | ● | 文件元数据改变 |
| IN_CLOSE_WRITE | ● | ● | 关闭为了写入而打开的文件 |
| IN_CLOSE_NOWRITE | ● | ● | 关闭以只读方式打开的文件 |
| IN_CREATE | ● | ● | 在受监控目录内创建了文件/目录 |
| IN_DELETE | ● | ● | 在受监控目录内删除文件/目录 |
| IN_DELETE_SELF | ● | ● | 删除受监控目录/文件本身 |
| IN_MODIFY | ● | ● | 文件被修改 |
| IN_MOVE_SELF | ● | ● | 移动受监控目录/文件本身 |
| IN_MOVED_FROM | ● | ● | 文件移出到受监控目录之外 |
| IN_MOVED_TO | ● | ● | 将文件移入受监控目录 |
| IN_OPEN | ● | ● | 文件被打开 |
| IN_ALL_EVENTS | ● | 以上所有输出事件的统称 | |
| IN_MOVE | ● | IN_MOVED_FROM |IN_MOVED_TO 事件的统称 | |
| IN_CLOSE | ● | IN_CLOSE_WRITE |IN_CLOSE_NOWRITE 事件的统称 | |
| IN_DONT_FOLLOW | ● | 不对符号链接解引用(始于 Linux 2.6.15) | |
| IN_MASK_ADD | ● | 将事件追加到 pathname 的当前监控掩码 | |
| IN_ONESHOT | ● | 只监控 pathname 的一个事件 | |
| IN_ONLYDIR | ● | pathname 不为目录时会失败(始于 Linux 2.6.15) | |
| IN_IGNORED | ● | 监控项为内核或应用程序所移除 | |
| IN_ISDIR | ● | name 中所返回的文件名为路径 | |
| IN_Q_OVERFLOW | ● | 事件队列溢出 | |
| IN_UNMOUNT | ● | 包含对象的文件系统遭卸载 |
对于上表所列出的绝大多数位而言,顾名便可知义。以下是对一些细节的澄清。
- 当文件的元数据(比如,权限、所有权、链接计数、扩展属性、用户 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 则规定,若 pathname 为符号链接,则不对其解引用。其作用在 于令应用程序可以监控符号链接,而非符号连接所指代的文件。
- 倘若对已为同一 inotify 描述符所监控的同一路径名再次执行 inotify_add_watch()调用, 那么默认情况下会用给定的 mask 掩码来替换该监控项的当前掩码。如果指定了 IN_MASK_ADD,那么则会将 mask 值与当前掩码相或。
- IN_ONESHOT 允许应用只监控 pathname 的一个事件。事件发生后,监控项会自动从监控列表中消失。
- 只有当 pathname 为目录时,IN_ONLYDIR 才允许应用程序对其进行监控。如果 pathname 并非目录,那么调用 inotify_add_watch()失败,报错为 ENOTDIR。如要确保 监控对象为一目录,则使用该标志可以规避竞争条件的发生。
5. 读取 inotify 事件
将监控项在监控列表中登记后,应用程序可用 read()从 inotify 文件描述符中读取事件,以 判定发生了哪些事件。若时至读取时尚未发生任何事件,read()会阻塞下去,直至有事件产生(除非对该文件描述符设置了 O_NONBLOCK 状态标志,这时若无任何事件可读,read()将立即失败, 并报错 EAGAIN)。
/* Structure describing an inotify event. */
struct inotify_event
{
int wd; /* Watch descriptor. */
uint32_t mask; /* Watch mask. */
uint32_t cookie; /* Cookie to synchronize two events. */
uint32_t len; /* Length (including NULs) of name. */
char name __flexarr; /* Name. */
};
-
字段 wd 指明发生事件的是那个监控描述符,该字段值由之前对 inotify_add_watch()的调 用返回。当应用程序要监控同一 inotify 文件描述符下的多个文件和目录时,字段 wd 就派上用场 。应用利用其所提供的线索来判定发生事件的特定文件或目录(要做到这一点,应用程序必须维护专有数据结构,记录监控描述符与路径名之间的关系。)
-
mask 字段会返回描述该事件的位掩码。由表所示的 Out 列展示了可出现于 mask 中 的位范围。还要注意下列与特殊位相关的更多细节。
移除监控项时,会产生 IN_IGNORED 事件。起因可能有两个:
-
其一,应用程序使用了 inotify_rm_watch()系统调用显式移除监控项;
-
其二,因受监控对象被删除或其所驻留的文件系统遭卸载,致使内核隐式删除监控项。以 IN_ONESHOT 而建立的监控项因事件触发而遭自动移除时,不会产生 IN_IGNORED 事件。
-
如果事件的主体为路径,那么除去其他位以外,在 mask 中还会设置 IN_ISDIR 位。
-
IN_UNMOUNT 事件会通知应用程序包含受监控对象的文件系统已遭卸载。该事件发 生之后,还会产生包含 IN_IGNORED 置位的附加事件。
-
使用 cookie 字段可将相关事件联系在一起。目前,只有在对文件重命名时才会用到该字 段。当这种情况发生时,系统会针对待重命名文件所在目录产生 IN_MOVED_FROM 事件, 然后,还会针对重命名后文件的所在目录生成 IN_MOVED_TO 事件。(若仅是在同一目录内 为文件改名,系统则会针对同一目录产生上述两个事件。)两个事件的 cookie 字段值相等,故 而应用程序得以将它们关联起来。
-
len 字段用于表示实际分配给 name 字段的字节数。在 read()所返回的缓冲区中,存储于 name 内的字符串结尾与下一个 inotify_event 结构的开始之间,可能会有额外 填充字节,故而 len 字段不可或缺。单个 inotify 事件的长度是 sizeof(struct inotify_event)+ len。
-
当受监控目录中有文件发生事件时,name 字段返回一个以空字符结尾的字符串,以标识 该文件。若受监控对象自身有事件发生,则不使用 name 字段,将 len 字段置 0。
如果传递给 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(),将在已发生事件数量与缓冲区可容纳事件数量间取最小值并返回之。
针对文件描述符 fd 调用 ioctl(fd, FIONREAD, &numbytes),会返回其所指代的 inotify 实例 中的当前可读字节数。
从 inotify 文件描述符中读取的事件形成了一个有序队列。打个比方,这样一来,对文件 重命名时,便可保证在 IN_MOVED_TO 事件之前能读取到 IN_MOVED_FROM 事件。 在事件队列的末尾追加一个新事件时,如果此新事件与队列当前的尾部事件拥有相同的 wd、mask、cookie 和 mask 值,那么内核会将两者合并(以避免对新事件排队)。之所以这么 做,是因为很多应用程序都并不关注同一事件的反复出现,而丢弃多余的事件能降低内核维 护事件队列所需的内存总量。然而,这也意味着使用 inotify 将无法可靠判定出周期性事件的 发生次数或频率。
6. 队列限制和/proc 文件
对 inotify 事件做排队处理,需要消耗内核内存。正因如此,内核会对 inotify 机制的操作 施以各种限制。超级用户可配置/proc/sys/fs/inotify 路径中的 3 个文件来调整这些限制:
- max_queued_events 调用 inotify_init()时,使用该值来为新 inotify 实例队列中的事件数量设置上限。一旦超出 这一上限,系统将生成 IN_Q_OVERFLOW 事件,并丢弃多余的事件。溢出事件的 wd 字段值 为−1。
- max_user_instances 对由每个真实用户 ID 创建的 inotify 实例数的限制值。
- max_user_watches 对由每个真实用户 ID 创建的监控项数量的限制值。
这 3 个文件的典型默认值分别为 16384、128 和 8192。
使用示例
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <string.h>
static void
handle_events(int fd, int *wd, int argc, char *argv[]) {
char buf[4096]
__attribute__ ((aligned(__alignof__(struct inotify_event))));
const struct inotify_event *event;
int i;
ssize_t len;
char *ptr;
/* 从 inotify 文件描述符无限循环读取事件. */
for (;;) {
len = read(fd, buf, sizeof buf);
if (len == -1 && errno != EAGAIN) {
perror("read");
exit(EXIT_FAILURE);
}
/*如果非阻塞的read()没有发现要读取的事件,则返回-1并将errno设置为EAGAIN。
在这种情况下,我们退出循环 */
if (len <= 0)
break;
/* 在buf中轮询所有事件 */
for (ptr = buf; ptr < buf + len;
ptr += sizeof(struct inotify_event) + event->len) {
event = (const struct inotify_event *) ptr;
/* 打印事件类型 */
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: ");
/* 打印监听目录*/
for (i = 1; i < argc; ++i) {
if (wd[i] == event->wd) {
printf("%s/", argv[i]);
break;
}
}
/* 打印文件名*/
if (event->len)
printf("%s", event->name);
/* 打印目录或者文件 */
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");
/* 创建用于访问inotify API的文件描述符 */
fd = inotify_init1(IN_NONBLOCK);
if (fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
/* 为监视描述符分配内存 */
wd = static_cast<int *>(calloc(argc, sizeof(int)));
if (wd == NULL) {
perror("calloc");
exit(EXIT_FAILURE);
}
// 命令行参数个数 argc ,argv[i] 是文件路径
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);
}
}
/* 准备 polling */
nfds = 2;
/* 控制台输入 可读 */
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
/* Inotify 文件描述符可读 */
fds[1].fd = fd;
fds[1].events = POLLIN;
printf("Listening for events.\n");
while (1) {
// 轮询控制台 和 Inotify 文件描述符 可读
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) {
/* 控制台如果有输入 结束循环*/
while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
continue;
break;
}
// Inotify 文件描述符有可读事件
if (fds[1].revents & POLLIN) {
// 处理文件路径事件
handle_events(fd, wd, argc, argv);
}
}
}
printf("Listening for events stopped.\n");
/* 关闭 inotify 文件描述符*/
close(fd);
free(wd);
exit(EXIT_SUCCESS);
}