使用 inotify 监控目录和文件的状态

概述

很多时候程序需要对文件状态进行监控,例如在命令行中删掉一个文件,图形界面中的文件管理器也要把这个文件去掉(如果文件管理器正在显示被删除文件所在的目录);如果试图用 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)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注