Unix/Linux编程:监控文件事件

本文详细介绍了Linux的inotify机制,一种用于监控文件系统事件的API。从inotify_init()和inotify_add_watch()的使用,到通过read()获取事件通知,以及不同事件位的解释,展示了如何实现文件和目录的监控。inotify机制允许应用程序检测文件或目录的创建、删除、修改等变化,是Linux系统中实现文件系统监控的重要工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 某些应用程序需要对文件或目录进行监控,已侦测是否发生了特定事件。比如,当把文件加入或者移除一目录树时,图形化文件管理器应该能判断此目录是否在当前显示之列;守护进程可能想要监控自己的配置文件,以了解其是否被修改
  • 自内核2.6.13起,Linux开始提供 inotify 机制,以允许应用程序监控文件事件
  • inotify 机制所取代的是 dnotify,后者一来较为陈旧,二来仅具备前者的部分功能

概述

使用inotify API由如下几个关键步骤

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

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

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

可以使用select()、poll()、epill以及由信号驱动的IO(自 Linux 2.6.25 起)来监控inotify文件描述符

inotify 机制属可选的 Linux 内核组件,可通过 CONFIG_INOTIFY 和 CONFIG_ INOTIFY_USER 选项进行配置

inotify API

NAME
       inotify_init, inotify_init1 - 初始化一个inotify实例 

SYNOPSIS
       #include <sys/inotify.h>

       int inotify_init(void);
       int inotify_init1(int flags);

DESCRIPTION
       inotify_init() 初始化新的inotify实例,并返回与新的inotify事件队列关联的文件描述符。 

       如果 flags = 0, 那么inotify_init1()相当于 inotify_init().  可以对标志中的以下值进行按位或运算,以获取不同的行为: 

       IN_NONBLOCK在新的打开文件说明中设置O_NONBLOCK文件状态标志。 使用此标志可以节省对fcntl(2)的额外调用,以实现相同的结果。

       IN_CLOEXEC在新文件描述符上设置执行关闭(FD_CLOEXEC)标志。 有关为什么可能有用的原因,请参见open(2)中O_CLOEXEC标志的描述。

RETURN VALUE
       成功时,这些系统调用将返回一个新的文件描述符。 
       发生错误时,返回-1,并设置errno表示错误 

VERSIONS
       inotify_init()首次出现在Linux 2.6.13中; 库支持已添加到版本2.4中的glibc。 
       inotify_init1() 是在Linux 2.6.27中添加的; 库支持已在2.9版中添加到glibc。 

inotify()系统调用可以创建一个新的inotify实例,返回回一个文件描述符(句柄),用于在后续操作中指代此inotify 实例

Linux 自内核 2.6.27 开始支持一个新的、非标准的系统调用 inotify_init1()。该系统调所执行的任务与 inotify_init()相同,但提供了一个额外的参数 flag,用于修改系统调用的行为。该参数支持的标志有二:

  • IN_CLOEXEC 标志会使内核针对新文件描述符激活 close-on-exec标志(FD_CLOEXEC)。引入该标志的原因 open()的 O_CLOEXEC 标志一样。
  • IN_NONBLOCK 标志会导致内核激活底层打开文件描述的 O_NONBLOCK 标志,如此一来,未来的读操作将是非阻塞式的,省得还要额外调用 fcntl()来获得相同效果

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

NAME
       inotify_add_watch - add a watch to an initialized inotify instance

SYNOPSIS
       #include <sys/inotify.h>

       int inotify_add_watch(int fd, const char *pathname, uint32_t mask);

DESCRIPTION
       inotify_add_watch() 为在路径名中指定位置的文件添加新监视或修改现有监视; 调用者必须对此文件具有读取权限。 

	   fd参数是一个文件描述符,它引用要修改其监视列表的inotify实例。

       在mask 参数中指定要监视的路径名事件。 有关可在掩码中设置的位的说明,请参见inotify(7)

       成功调用inotify_add_watch()会返回与此inotify实例的路径名关联的唯一监视描述符。 如果此inotify实例先前未
       监视路径名,那么将重新分配监视描述符。 如果已经监视路径名,则返回现有监视的描述符。 

       监视描述符由inotify文件描述符中的稍后read(2)返回。 这些读取将获取指示文件系统事件的inotify_event结构
       (请参见inotify(7))。 该结构中的监视描述符标识发生事件的对象。 

RETURN VALUE
       成功时,inotify_add_watch()返回一个非负的监视描述符。 发生错误-1时,将正确设置errno。 

  • 参数 pathname 标识欲创建或修改的监控项所对应的文件。调用程序必须对该文件具有读权限(调用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所定义的监控项

NAME
       inotify_rm_watch - 从inotify实例中删除现有监控项

SYNOPSIS
       #include <sys/inotify.h>

       int inotify_rm_watch(int fd, int wd);

DESCRIPTION
       inotify_rm_watch() 从与文件描述符fd关联的inotify实例中删除与监视描述符wd关联的监视。 

       删除监视将导致为此监视描述符生成IN_IGNORED事件 .  (See inotify(7).)

RETURN VALUE
       成功时,inotify_rm_watch()返回零;如果发生错误,则返回-1(在这种情况下,将正确设置errno)。 

  • 参数 wd 是一监控描述符,由之前对 inotify_add_watch()的调用返回。(uint32_t 数据类型为一无符号 32 位整数。)
  • 删除监控项会为该监控描述符生成 IN_IGNORED 事件。

inotify 事件

使用inotify_add_watch()添加或者修改监控项时,位掩码mask标识了针对给定路径名(pathname)要监控的事件。下表列出来可以在mask中定义的事件位:

InOut描述
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_MOVEIN_MOVED_FROM , IN_MOVED_TO 事件的统称
IN_CLOSEIN_CLOSE_WRITE , IN_CLOSE_NOWRITE 事件的统称
IN_DONT_FOLLOW不对符号链接解引用(始于 Linux 2.6.15)
IN_MASK_ADD将事件追加到 pathname 的当前监控掩码
IN_ONESHOT只监控 pathname 的一个事件
IN_ONLYDIRpathname 不为目录时会失败(始于 Linux 2.6.15)
IN_IGNORED监控项为内核或应用程序所移除
IN_ISDIRname 中所返回的文件名为路径
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;
	uint32_t mask;
	uint32_t cookie;
	uint32_t len;
	char 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 置位的附加事件。
    • IN_Q_OVERFLOW
  • cookie字段可将相关事件联系在一起。目前,只有在对文件重命名时才会用到该字段

    • 当这种情况发生时,系统会针对待重命名文件所在目录产生 IN_MOVED_FROM 事件,然后,还会针对重命名后文件的所在目录生成 IN_MOVED_TO 事件。
    • 两个事件的 cookie 字段值相等,故而应用程序得以将它们关联起来。
    • 若仅是在同一目录内为文件改名,系统则会针对同一目录产生上述两个事件。
  • name:

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

    • 用于表示实际分配给name字段的字节数
    • 在read()所返回的缓冲区中,存储于name内的字符串结尾与下一个inotify_event结构的开始之间,可能会有额外填充字节,故而 len 字段不可或缺。单个 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()中读取多个事件,效率极高。

针对文件描述符 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 创建的监控项数量的限制值

这 3 个文件的典型默认值分别为 16384、128 和 8192。

示例

#include <sys/inotify.h>
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <zconf.h>

static void             /* Display information from inotify_event structure */
displayInotifyEvent(struct inotify_event *i)
{
    printf("    wd =%2d; ", i->wd);
    if (i->cookie > 0)
        printf("cookie =%4d; ", i->cookie);

    printf("mask = ");
    if (i->mask & IN_ACCESS)        printf("IN_ACCESS ");
    if (i->mask & IN_ATTRIB)        printf("IN_ATTRIB ");
    if (i->mask & IN_CLOSE_NOWRITE) printf("IN_CLOSE_NOWRITE ");
    if (i->mask & IN_CLOSE_WRITE)   printf("IN_CLOSE_WRITE ");
    if (i->mask & IN_CREATE)        printf("IN_CREATE ");
    if (i->mask & IN_DELETE)        printf("IN_DELETE ");
    if (i->mask & IN_DELETE_SELF)   printf("IN_DELETE_SELF ");
    if (i->mask & IN_IGNORED)       printf("IN_IGNORED ");
    if (i->mask & IN_ISDIR)         printf("IN_ISDIR ");
    if (i->mask & IN_MODIFY)        printf("IN_MODIFY ");
    if (i->mask & IN_MOVE_SELF)     printf("IN_MOVE_SELF ");
    if (i->mask & IN_MOVED_FROM)    printf("IN_MOVED_FROM ");
    if (i->mask & IN_MOVED_TO)      printf("IN_MOVED_TO ");
    if (i->mask & IN_OPEN)          printf("IN_OPEN ");
    if (i->mask & IN_Q_OVERFLOW)    printf("IN_Q_OVERFLOW ");
    if (i->mask & IN_UNMOUNT)       printf("IN_UNMOUNT ");
    printf("\n");

    if (i->len > 0)
        printf("        name = %s\n", i->name);
}

#define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))
int
main(int argc, char *argv[])
{
    int inotifyFd, wd, j;
    char buf[BUF_LEN] __attribute__ ((aligned(8)));
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    if (argc < 2 || strcmp(argv[1], "--help") == 0){
        printf("%s pathname...\n", argv[0]);
        exit(EXIT_FAILURE);
    }


    inotifyFd = inotify_init();                 /* Create inotify instance */
    if (inotifyFd == -1){
        perror("inotify_init");
        exit(EXIT_FAILURE);
    }


    /* For each command-line argument, add a watch for all events */

    for (j = 1; j < argc; j++) {
        wd = inotify_add_watch(inotifyFd, argv[j], IN_ALL_EVENTS); //每个监控项都将监控所有可能发生的事件。
        if (wd == -1){
            perror("inotify_add_watch");
            exit(EXIT_FAILURE);
        }

        printf("Watching %s using wd %d\n", argv[j], wd);
    }

    for (;;) {                                  /* Read events forever */
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0) {
            perror("read() from inotify fd returned 0!");
            exit(EXIT_FAILURE);
        }
        if (numRead == -1){
            perror("read");
            exit(EXIT_FAILURE);
        }

        /*FIXME: should use %zd here, and remove (long) cast */
        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        /* Process all of the events in buffer returned by read() */

        for (p = buf; p < buf + numRead; ) {
            event = (struct inotify_event *) p;
            displayInotifyEvent(event);

            p += sizeof(struct inotify_event) + event->len;
        }
    }

    exit(EXIT_SUCCESS);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值