概述
很多时候程序需要对文件状态进行监控,例如在命令行中删掉一个文件,图形界面中的文件管理器也要把这个文件去掉(如果文件管理器正在显示被删除文件所在的目录);如果试图用 vim 保存一个文件,而该文件在修改的时候被其它程序修改过,那么 vim 会给出警告说文件已经发生改变。如果需要对文件状态进行实时监控,一个方法是保存文件的状态,然后不停地扫描目录或文件,如果和上一次保存的状态不一致则发出信号,但是这样的做法效率很低。最好的做法是能够向系统注册一个回调函数,当我们需要监控的目录或文件发生改变时调用回调函数,这样程序就能马上得到通知。
根据参考资料 [1],inotify 之前的文件状态监控机制是 dnotify。dnotify 中的“d”指的是目录,只能监控目录事件的变化,即在该目录下的创建/删除文件引起的事件,但是却不能监控文件本身状态的改变(如读写/访问文件),如果需要实现这样的功能需要应用程序自己比较文件状态。
在 2.6.13 的时候引入的 inotify 提供了比 dnotify 更强大的功能,除了目录还可以对文件状态进行监控。并且它不像 dnotify,每监控一个目录/文件都需要打开一个文件描述符,因此不会影响移动介质的 unmount。从接口形式和功能上看,inotify 和 epoll 很像,只是两者监控的事件类型不一样。
09 年出现了 fanotify(参考资料 [2]),据说是 inotify 的下一代,不过目前来看功能还不如 inotify。
对目录的监控
作为 inotify 的第一个示例,程序会在 /tmp/abc 被创建/删除/移动时打印相应的信息:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/inotify.h>
#define EVENT_BUFSIZE 4096
int main(void)
{
int len = 0;
int fd, dir_wd;
const char* dirname = "/tmp";
char buf[EVENT_BUFSIZE], *cur = buf, *end;
fd = inotify_init();
if (fd == -1) {
perror("inotify_init");
return -1;
}
dir_wd = inotify_add_watch(fd, dirname,
IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_MOVED_FROM);
if (dir_wd == -1) {
perror("inotify_add_watch");
goto end;
}
while (1) {
len = read(fd, cur, EVENT_BUFSIZE - len);
if (len <= 0) {
perror("read inotify event");
goto end;
}
end = cur + len;
while (cur + sizeof(struct inotify_event) <= end) {
struct inotify_event* e = (struct inotify_event*)cur;
if (cur + sizeof(struct inotify_event) + e->len > end)
break;
if (e->mask & IN_CREATE) {
if (e->mask & IN_ISDIR)
printf("directory %s is created.\n", e->name);
else
printf("file %s is created.\n", e->name);
}
if (e->mask & IN_DELETE)
printf("dentry %s is deleted.\n", e->name);
if (e->mask & IN_MOVED_FROM)
printf("dentry %s is moved to other place.\n", e->name);
if (e->mask & IN_MOVED_TO)
printf("dentry %s is moved here from other place.\n", e->name);
cur += sizeof(struct inotify_event) + e->len;
}
if (cur >= end) {
cur = buf;
len = 0;
} else {
len = end - cur;
memmove(buf, cur, len);
cur = buf + len;
}
}
end:
close(fd);
return 0;
}
程序首先调用
int inotify_init(void);
返回一个文件描述符 fd,事件消息被抽象成文件数据流,后续的操作都基于标准的文件系统 api。这里可以看成是 inotify_init() 生成了一个事件组,fd 为这个组的描述符。
要为某个目录或文件添加监控,使用函数
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
其中第一个参数是由 inotify_init() 返回的文件描述符,第二个参数是需要监控的文件路径,第三个参数是需要监控的事件定义,在参考资料 [3] 中有详细描述,另外在头文件 sys/inotify.h 中的定义为:
/* Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH. */
#define IN_ACCESS 0x00000001 /* File was accessed. */
#define IN_MODIFY 0x00000002 /* File was modified. */
#define IN_ATTRIB 0x00000004 /* Metadata changed. */
#define IN_CLOSE_WRITE 0x00000008 /* Writtable file was closed. */
#define IN_CLOSE_NOWRITE 0x00000010 /* Unwrittable file closed. */
#define IN_CLOSE (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) /* Close. */
#define IN_OPEN 0x00000020 /* File was opened. */
#define IN_MOVED_FROM 0x00000040 /* File was moved from X. */
#define IN_MOVED_TO 0x00000080 /* File was moved to Y. */
#define IN_MOVE (IN_MOVED_FROM | IN_MOVED_TO) /* Moves. */
#define IN_CREATE 0x00000100 /* Subfile was created. */
#define IN_DELETE 0x00000200 /* Subfile was deleted. */
#define IN_DELETE_SELF 0x00000400 /* Self was deleted. */
#define IN_MOVE_SELF 0x00000800 /* Self was moved. */
/* Events sent by the kernel. */
#define IN_UNMOUNT 0x00002000 /* Backing fs was unmounted. */
#define IN_Q_OVERFLOW 0x00004000 /* Event queued overflowed. */
#define IN_IGNORED 0x00008000 /* File was ignored. */
/* Helper events. */
#define IN_CLOSE (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE) /* Close. */
#define IN_MOVE (IN_MOVED_FROM | IN_MOVED_TO) /* Moves. */
/* Special flags. */
#define IN_ONLYDIR 0x01000000 /* Only watch the path if it is a
directory. */
#define IN_DONT_FOLLOW 0x02000000 /* Do not follow a sym link. */
#define IN_EXCL_UNLINK 0x04000000 /* Exclude events on unlinked
objects. */
#define IN_MASK_ADD 0x20000000 /* Add to the mask of an already
existing watch. */
#define IN_ISDIR 0x40000000 /* Event occurred against dir. */
#define IN_ONESHOT 0x80000000 /* Only send event once. */
/* All events which a program can wait on. */
#define IN_ALL_EVENTS (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE \
| IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM \
| IN_MOVED_TO | IN_CREATE | IN_DELETE \
| IN_DELETE_SELF | IN_MOVE_SELF)
如果需要监控多个事件,只需将事件取或就行,例如程序中监控了目录中的创建/删除/移动事件,只要有其中一个事件发生都会触发 inotify。函数的返回值是一个 wd(watch descriptor),与添加的文件及事件相关联。
函数主体是一个 while(1) 循环,使用的是系统调用 read() 阻塞在 fd 上,获取消息事件通知。当 inotify_add_watch() 添加的某个目录或文件发生了指定的事件时,read() 会把相应的事件内容读到 buf 中,其中的内容是若干个 struct inotify_event 结构体:
struct inotify_event {
int wd; /* Watch descriptor */
uint32_t mask; /* Mask of events */
uint32_t cookie; /* Unique cookie associating related events (for rename(2)) */
uint32_t len; /* Size of name field */
char name[]; /* Optional null-terminated name */
};
其中的 wd 字段是由 inotify_add_watch() 返回的描述符;mask 是在该描述符上发生的事件集合;cookie 字段用于把 rename() 行为连接起来,也就是说如果把监控的目录下的 A 重命名为 B,则会产生两个事件 IN_MOVED_FROM 和 IN_MOVED_TO,这两个事件的 cookie 值是一样的;name 是一个变长数组,长度由 len 指定,因此读取 buf 的时候要根据 len 跳过相应的长度。不过 name 是以 '\0' 结尾的字符串,其有效长度不一定就是 len,可能会有 padding(见参考资料 [3] 关于 name 字段的说明)。
对于结构体中的 mask 字段,在参考资料 [3] 中有这样的描述:
If successive output inotify events produced on the inotify file descriptor are identical (same wd, mask, cookie, and name) then they are coalesced into a single event if the older event has not yet been read (but see BUGS).
所以在事件判断中还是不要用“if...else if”这样的逻辑比较好,否则有可能漏掉一些事件。由于例子里只有一个事件,因此也没有对 wd 的判断。
如果在监控的目录下创建/删除/移动同名目录 /tmp/abc 也会触发事件,因为 inotify 并不区分触发事件的 dentry 类型(上面的程序只在创建的事件加了判断)。
最后如果需要结束监控,只需使用 close() 关闭 fd 即可。
对普通文件状态的监控
inotify 相对于 dnotify 的一个优点是可以对文件的状态进行监控。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/inotify.h>
#define EVENT_BUFSIZE 4096
int main(void)
{
int len = 0;
const char* dirname = "/tmp";
const char* filename = "abc";
char buf[EVENT_BUFSIZE], *cur = buf, *end;
int fd, dir_wd, file_wd;
char path[256];
sprintf(path, "%s/%s", dirname, filename);
fd = inotify_init();
if (fd == -1) {
perror("inotify_init");
return -1;
}
dir_wd = inotify_add_watch(fd, dirname,
IN_CREATE | IN_MOVED_TO | IN_MOVED_FROM);
if (dir_wd == -1) {
perror("inotify_add_watch");
goto end;
}
file_wd = inotify_add_watch(fd, path, IN_CLOSE_WRITE | IN_DELETE_SELF);
while (1) {
len = read(fd, cur, EVENT_BUFSIZE - len);
if (len <= 0) {
perror("read inotify event");
goto end;
}
end = cur + len;
while (cur + sizeof(struct inotify_event) <= end) {
struct inotify_event* e = (struct inotify_event*)cur;
if (cur + sizeof(struct inotify_event) + e->len > end)
break;
if (e->mask & IN_CREATE) {
if (strcmp(e->name, filename) != 0)
goto next;
if (e->mask & IN_ISDIR)
goto next;
printf("file %s is created.\n", filename);
file_wd = inotify_add_watch(fd, path,
IN_CLOSE_WRITE | IN_DELETE_SELF);
}
if (e->mask & IN_MOVED_FROM) {
if (strcmp(e->name, filename) != 0)
goto next;
if (e->mask & IN_ISDIR)
goto next;
printf("file %s is moved to other place.\n", filename);
inotify_rm_watch(fd, file_wd);
}
if (e->mask & IN_MOVED_TO) {
if (strcmp(e->name, filename) != 0)
goto next;
if (e->mask & IN_ISDIR)
goto next;
printf("file %s is moved here from other place.\n", filename);
file_wd = inotify_add_watch(fd, path,
IN_CLOSE_WRITE | IN_DELETE_SELF);
}
if (e->mask & IN_CLOSE_WRITE)
printf("file %s is modified.\n", e->name);
if (e->mask & IN_DELETE_SELF) {
if (e->mask & IN_ISDIR)
goto next;
printf("file %s is deleted.\n", filename);
inotify_rm_watch(fd, file_wd);
}
next:
cur += sizeof(struct inotify_event) + e->len;
}
if (cur >= end) {
cur = buf;
len = 0;
} else {
len = end - cur;
memmove(buf, cur, len);
cur = buf + len;
}
}
end:
close(fd);
return 0;
}
为了监控文件被创建的事件,除了对文件本身进行监控外,对文件所在的目录也做了监控。当被监控的文件被创建时,使用了 inotify_add_watch() 把新的 wd 加入监控列表;当被监控的文件被删除时,使用函数
int inotify_rm_watch(int fd, int wd);
把对应的 wd 从监控列表中移除。
由于程序中只对一个文件进行监控,因此省略了对 wd 的比较判断。如果对同一个存在的文件多次调用 inotify_add_watch(),返回的 wd 都是一样的值,但是如果文件被删除又重新加入,则 wd 会有变化,因为其对应的 inode 已经不一样了。
为了监控文件是否被修改,这里使用了 IN_CLOSE_WRITE 而不是 IN_MODIFY。因为文件的每次更新都会引发 IN_MODIFY 事件(例如我们编辑文件时随手保存的行为,这取决于编辑器是否立即写入更新),而 IN_CLOSE_WRITE 则只会在文件被关闭并且被更新的时候才会被触发,这样能避免很多文件编辑过程中的更新消息。另外在测试的时候发现,每次文件被修改都会触发两次 IN_MODIFY 事件,但是 IN_CLOSE_WRITE 则只会触发一次,暂时不清楚是什么原因。
一些 inotify 相关的 faq 可以参考一下参考资料 [4]。
最后写了个比较实用的小程序,放在 这里,可以用来监控配置文件的变化从而避免服务程序的重启。
参考资料
[1] inotify -- Linux 2.6 内核中的文件系统变化通知机制
[2] fanotify 监控文件系统
[3] inotify(7)
[4] inotify FAQ (Frequently Asked Questions)