• 监控文件事件


    【摘自《Linux/Unix系统编程手册》】

    自内核2.6.13起,Linux开始提供inotify机制,以允许应用程序监控文件事件。

    使用inotify API有以下几个关键步骤:

    1. 应用程序使用 inotify_init() 来创建一个inotify实例,该系统调用所返回的文件描述符用于在后续操作中指代该实例。
    2. 应用程序使用 inotify_add_watch() 向inotify实例的监控列表添加条目,藉此告诉内核哪些文件是自己的兴趣所在。每个监控项都包含一个路径名以及相关的位掩码。位掩码针对路径名指明了所要监控的事件集合。作为函数结果,inotify_add_watch()将返回一监控描述符,用于在后续操作中指代该监控项(系统调用 inotify_rm_watch() 执行其逆向操作,将之前添加入 inotify 实例的监控项移除)。
    3. 为获得事件通知,应用程序需针对 inotify 文件描述符执行 read() 操作。每次对 read() 的成功调用,都会返回一个或多个 inotify_event 结构,其中各自记录了处于 inotify 实例监控之下的某一路径名所发生的事件。
    4. 应用程序在结束监控时会关闭 inotify 文件描述符。这会自动清除与 inotify 实例相关的所有监控项。

    inotify 机制可用于监控文件或目录。当监控目录时,与路径自身及其所含文件相关的事件都会通知给应用程序。

    inotify 监控机制为非递归。若应用程序有意监控整个目录子树内的事件,则需对该树中的每个目录发起 inotify_add_watch() 调用。

    inotify API

    #include <sys/inotify.h>
    
    int inotify_init(void);
                                    Returns file descriptor on success, or -1 on error
    
    int inotify_add_watch(int fd, const char* pathname, uint32_t mask);
                                    Returns watch descriptor on success, or -1 on error
    
    int inotify_rm_watch(int fd, uint32_t wd);
                                    Returns 0 on success, or -1 on error

    针对文件描述符 fd 所指代的 inotify 实例的监控列表,系统调用 inotify_add_watch() 既可以追加新的监控项,也可以修改现有的监控项。

    调用程序必须对该文件具有读权限。调用 inotify_add_watch() 时,会对文件权限做一次性检查。只要监控项继续存在,即便有人更改了文件权限,使调用程序不再对文件具有权限,调用程序依然会继续受到文件的通知消息。

    参数 mask 为一位掩码,针对 pathname 定义意欲监控的事件。如果先前未将 pathname 加入 fd 的监控列表,那么 inotify_add_watch() 会在列表中穿件一个新的监控项,并返回一新的、非负监控描述符,用来在后续操作中指代此监控项。对 inotify 实例来说,该监控描述符是唯一的。

    若先前已将 pathname 加入 fd 的监控列表,则 inotify_add_watch() 会修改现有 pathname 监控项的掩码,并返回其监控描述符。(此描述符就是最初将 pathname 加入该监控列表的系统调用 inotify_add_watch() 所返回的监控描述符。)

    系统调用 inotify_rm_watch() 会从文件描述符 fd 所指代的 inotify 实例中,删除有 wd 所定义的监控项。

    参数 wd 是一监控描述符,由之前对 inotify_add_watch() 的调用返回。

    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。如要确保监控对象为一目录,则使用该标志可以规避竞争条件的发生

    读取 inotify 事件

    将监控项在监控列表中登记后,应用程序可用 read() 从 inotify 文件描述符中读取事件,以判定发生了哪些事件。若时至读取时尚未发生任何事件,read() 会阻塞下去,直至有事件产生(除非对该文件描述符设置了O_NONBLOCK状态标识,这时若无任何事件可读,read()将立即失效,并报错EAGAIN)。

    事件发生后,每次调用 read() 会返回一个缓冲区,内含一个或多个如下类型的结构

    struct inotify_event {
       int       wd;         /* Watch descriptor on which event occurred */
       uint32_t  mask;       /* Bits describing event that occurred */
       uint32_t  cookie;     /* Cookie for related events (for rename()) */
       uint32_t  len;        /* Size of 'name' field */
       char      name[];     /* Optional null-terminated filename */
    };

    字段 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 字段值相等,故而应用程序得以将它们关联起来。

    当受监控目录中有文件发生事件时,name 字段返回一个以空字符结尾的字符串,以标识该文件。若受监控对象自身有事件发生,则不使用 name 字段,将 len字段置0。

    len 字段用于表示实际分配给 name 字段的字节数。在 read() 所返回的缓冲区中,存储于 name 内的字符串结尾与下一个 inotify_event 结构的开始之间,可能会有额外填充字节,故而 len 字段不可或缺。单个 inotify 事件的长度是 sizeof(struct inotify_event) + len 。

    如果传递给 read() 的缓冲区过小,无法容纳下一个 inotify_event 的结构,那么 read() 调用将以失败告终,并以 EINVAL 错误向应用程序报告这一情况。应用程序可再次以更大的缓冲区执行 read() 操作。然而,只要确保缓冲区足以容纳至少一个事件,这一问题将得以完全规避:传给 read() 的缓冲区应至少为 sizeof(struct inotify_event) + NAME_MAX + 1 字节,其中 NAME_MAX 是文件名的最大长度,此外再加上终止空字符使用的一个字节。

    采用的缓冲区大小如大于最小值,则可自单个 read() 中读取多个事件,效率极高。对 inotify 文件描述符所执行的 read(),将在已发生事件数量与缓冲区可容纳事件数量间取最小值并返回之。

    PS:针对文件描述符 fd 调用 ioctl(fd, FIONREAD, &numbytes),会返回其指代的 inotify 实例中的当前可读字节数。

    从 inotify 文件描述符中读取的事件形成一个有序队列。打个比方,这样一来,对文件重命名时,便可保证在 IN_MOVED_TO 事件之前能读取到 IN_MOVED_FROM 事件。

    在事件队列的末尾追加一个新事件时,如果此新事件与队列当前的尾部事件拥有相同的 wd、mask、cookie 和 mask值,那么内核会将两者合并(以避免对新事件排队)。之所以这么做,是因为很多应用程序都并不关注同一事件的反复出现,而丢弃多余的事件能降低内核维护事件队列所需的内存总量。然而,这也意味着使用 inotify 将无法可靠判定出周期性事件的发生次数或频率。

    队列限制和 /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 创建的监控项数量的限制值。

    示例:demo_inotify.c

     1 #include <sys/inotify.h>
     2 #include <limits.h>
     3 #include "tlpi_hdr.h"
     4 
     5 static void displayInotifyEvent(struct inotify_event* i)
     6 {
     7     printf("    wd = %2d; ", i->wd);
     8     if (i->cookie > 0)
     9         printf("cookie = %4d; ", i->cookie);
    10 
    11     printf("mask = ");
    12     if (i->mask & IN_ACCESS)
    13         printf("IN_ACCESS ");
    14     if (i->mask & IN_ATTRIB)
    15         printf("IN_ATTRIB ");
    16     if (i->mask & IN_CLOSE_NOWRITE)
    17         printf("IN_CLOSE_NOWRITE ");
    18     if (i->mask & IN_CLOSE_WRITE)
    19         printf("IN_CLOSE_WRITE ");
    20     if (i->mask & IN_CREATE)
    21         printf("IN_CREATE ");
    22     if (i->mask & IN_DELETE)
    23         printf("IN_DELETE ");
    24     if (i->mask & IN_DELETE_SELF)
    25         printf("IN_DELETE_SELF ");
    26     if (i->mask & IN_IGNORED)
    27         printf("IN_IGNORED ");
    28     if (i->mask & IN_ISDIR)
    29         printf("IN_ISDIR ");
    30     if (i->mask & IN_MODIFY)
    31         printf("IN_MODIFY ");
    32     if (i->mask & IN_MOVE_SELF)
    33         printf("IN_MOVE_SELF ");
    34     if (i->mask & IN_MOVE_SELF)
    35         printf("IN_MOVE_SELF ");
    36     if (i->mask & IN_MOVED_FROM)
    37         printf("IN_MOVED_FROM ");
    38     if (i->mask & IN_MOVED_TO)
    39         printf("IN_MOVED_TO ");
    40     if (i->mask & IN_OPEN)
    41         printf("IN_OPEN ");
    42     if (i->mask & IN_Q_OVERFLOW)
    43         printf("IN_Q_OVERFLOW ");
    44     if (i->mask & IN_UNMOUNT)
    45         printf("IN_UNMOUNT ");
    46     printf("
    ");
    47 
    48     if (i->len > 0)
    49         printf("    name = %s
    ", i->name);
    50 }
    51 #define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))
    52 
    53 int main(int argc, char* argv[])
    54 {
    55     int inotifyFd, wd, j;
    56     char buf[BUF_LEN];
    57     ssize_t numRead;
    58     char* p;
    59     struct inotify_event* event;
    60 
    61     if (argc < 2 || strcmp(argv[1], "--help") == 0)
    62         usageErr("%s pathname ... 
    ", argv[0]);
    63 
    64     inotifyFd = inotify_init();
    65     if (inotifyFd == -1)
    66         errExit("inotify_init");
    67 
    68     for (j = 1; j < argc; j++)
    69     {
    70         wd = inotify_add_watch(inotifyFd, argv[j], IN_ALL_EVENTS);
    71         if (wd == -1)
    72             errExit("inotify_add_watch");
    73 
    74         printf("Watching %s using wd %d
    ", argv[j], wd);
    75     }
    76 
    77     for (;;)
    78     {
    79         numRead = read(inotifyFd, buf, BUF_LEN);
    80         if (numRead == 0)
    81             fatal("read() from inotify fd return 0!");
    82 
    83         if (numRead == -1)
    84             errExit("read");
    85 
    86         printf("Read %ld bytes from inotify fd
    ", (long)numRead);
    87 
    88         for (p = buf; p < buf + numRead;)
    89         {
    90             event = (struct inotify_event*)p;
    91             displayInotifyEvent(event);
    92             p += sizeof(struct inotify_event) + event->len;
    93         }
    94     }
    95 
    96     exit(EXIT_SUCCESS);
    97 }
  • 相关阅读:
    「2017 山东三轮集训 Day1」Flair
    Luogu P4321 随机漫游
    「WC2018」通道
    「CTSC2018」暴力写挂
    关于二项式相乘
    BZOJ #3625 CF #438E 小朋友和二叉树
    GIS可视化
    微信小程序Promise对象
    SQL Server-执行计划教会我如何创建索引
    IIS+NGINX 负载web服务器
  • 原文地址:https://www.cnblogs.com/jingyg/p/5144933.html
Copyright © 2020-2023  润新知