Linux文件系统事件监控之inotify
2023-11-27 17:02:42 阿炯

在 Linux 中提供了 inotify 机制允许应用程序可以监听文件(目录)事件。本文主要从以下几个方面对 inotify 进行介绍:
简介及使用场景
机制关联的相关系统调用
支持的事件类型
用户态工具与使用示例及常见问题


简介及使用场景

1.inotify 是一个 Linux 内核特性(监视文件系统事件),它用于监控文件系统,比如删除、读、写操作等,当发生对应事件时,则会触发 inotify。当监控目录时,与该目录自身以及该目录下面的文件都会被监控,其上有事件发生时都会通知给应用程序

2.inotify 监控机制为非递归,若想监控整个目录子树内的事件,则需对该树中的每个目录发起 inotify_add_watch() 调用

3.使用 inotify:创建一个文件描述符,附加一个或多个监视器(一个监视器 是一个路径和一组事件),然后使用 read() 方法从描述符获取事件信息。read() 并不会用光整个周期,它在事件发生之前是被阻塞的。

4.因为 inotify 通过传统的文件描述符工作,可使用 select(),poll(),epoll() 以及由信号驱动的 I/O 来监控 inotify 文件描述符

5.要使用 inotify,必须具备一台带有 2.6.13 或更新内核的 Linux 机器(以前的 Linux 内核版本使用更低级的文件监控器 dnotify或通过打补丁的方式)

监听文件或者目录的变更,最终目的一定是基于不同的变更事件采取相对应的处理措施。比较常见的使用场景如下:
配置文件热加载:当配置文件发生变化时进程可以自动感知并重新 reload 配置文件
配置保持功能:当需要保持服务器上某些文件不被改动时可以监听需要保持的文件,当文件出现变更时做相应的恢复处理;当文件移出或者加入到某个目录下的时候,图形化文件管理器需要根据对应的事件作出相对应的调整

系统调用 与 inotify 有关的系统调用主要有三个:inotify_init,inotify_add_watch,inotify_rm_watch

inotify_init

#include <sys/inotify.h>
int inotify_init(void);

inotify_init创建一个inotify实例,该函数会返回文件描述符用来指代inotify实例,同时之后需要通过对该文件描述符进行read 操作获取文件变更事件。

返回值:
成功:该函数的返回值为一个文件描述符,该文件描述符所指代的文件中将会保存所监控的 文件/目录 所发生的 事件集。

失败:返回 -1,并且将 errno 设置为对应错误。

使用及解释:
int fd = inotify_init();
fd 为所指的 inotify 实例的 监控列表,系统调用 inotify_add_watch() 可以向该 fd 追加 新的监控项。


inotify_add_watch

#include <sys/inotify.h>
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);

fd 指代 inotify_init 系统调用返回的 notify 实例
pathname 指代需要被监听事件的文件或者目录路径
mask 事件掩码,表明需要监听的事件类型。具体的事件类型下文会进行描述

该系统调用返回值(wd)是监控描述符,指代一条监控项。

返回值:
成功:返回值为一个用于 唯一指代此 监控项 的描述符

失败:返回值 < 0 ,则代表添加该监控项失败,需要检测 pathname 是否有可读权限,是否存在,系统的监控队列是否已满等

参数:
pathname 为想要创建的监控项所对应的文件,特别注意调用该接口必须要对该文件有读权限,该函数只对文件做一次检查,如果在监控时修改了所监控的文件读权限,则不会影响继续监控此文件;

mask 为一位掩码,针对 pathname 定义了想要监控的事件,此函数的返回值为一个用于唯一指代此监控项的描述符。


上图展示了一个notify实例,以及该实例维护的一组监控项:
监控描述符就是 inotify_add_watch系统调用的返回,唯一指代一个监控描述项
掩码即是 mask 用来定义具体的监听事项
pathname 即是完整的待监听文件或目录的合法路径

inotify_rm_watch

#include <sys inotify.h="">
int inotify_rm_watch(int fd, uint32_t wd);
fd 指代 inotify_init 系统调用返回的 notify 实例
wd 指代监控项描述符

事件类型

常规事件类型
Mask标志    描述
IN_ACCESS    文件被访问(执行了 read 操作)
IN_ATTRIB    文件元数据发生变更
IN_CLOSE_WRITE    关闭为了写入而打开的文件
IN_CLOSE_NOWRITE    关闭以只读方式打开的文件
IN_CREATE    在受控目录内创建了文件或者目录
IN_DELETE    在受控目录内删除了文件或者目录
IN_DELETE_SELF    删除受控文件或者目录本身
IN_MOVED_FROM    文件移出受控目录之外
IN_MOVED_TO    文件移入受控目录
IN_OPEN    文件被打开
IN_MOVE    IN_MOVED_FROM|IN_MOVED_TO 事件的统称
IN_CLOSE    IN_CLOSE_WRITE|IN_CLOSE_NOWRITE 统称

IN_ATTRIB监控的元数据变更包括,权限,所有权,链接数,用户 ID 等

重命名受控对象时会发出IN_DELETE_SELF事件,而如果受控目标是一个目录,那么受控目标下的文件发生重命名时会触发两个事件IN_MOVED_FROM和IN_MOVED_TO

在日常开发工作中,上述事件已经基本涵盖了文件变更的所有情况。我们可以按照各自的场景,针对上述不同的事件类型做出相应的处理流程。


其它事件
除了上述文件的常规事件外,inotify还提供了以下几个 mask 来控制事件监听的过程
Mask标志    描述
IN_DONT_FOLLOW    不对符号链接引用
IN_MASK_ADD    将事件追加到 pathname 的当前监控掩码
IN_ONESHOT    只监控 pathname 的一个事件
IN_ONLYDIR    pathname 不为目录时会失败
将上述 mask 标志添加到 inotify_add_watch 中时可以控制监听过程,这么说有点笼统,举个例子来说。

inotify_add_watch(fd, pathname, IN_OPEN | IN_CLOSE | IN_ONESHOT);
上面这段代码,除了监听文件的 IN_OPEN和IN_CLOSE事件外,还添加了 IN_ONESHOT mask,那么这就意味着,当监听到 pathname 所指代的文件一次事件后 inotify就不会在监听 pathname 所指代的文件发出的事件了。

上述 mask 是在添加某个文件监控项的时候作为inotify_add_watch系统调用的参数传入的。除此之外还有以下几个事件,这些事件不需要用户显示调用inotify_add_watch添加,仅当出现一些其他异常情况时发出。

IN_ISDIR事件表明被监听的 pathname 指代的是一个目录,举例来说 mkdir /tmp/xxx 这个系统命令会产生 IN_CREATE|IS_DIR 事件。

常用监控事件(对上文提及的事件的小结)
IN_ACCESS:文件 被访问时 触发事件,例如 read,execve

IN_ATTRIB:文件属性 发生变化 触发事件。例如 权限 chmod,时间戳 setxattr,链接数 link 等

IN_CLOSE_WRITE:一个文件被打开 写入操作结束,文件被关闭时 触发事件

IN_CLOSE_NOWRITE:一个文件被打开 没有任何写操作,文件被关闭时 触发事件

IN_CREATE:在监控列表下 创建一个文件或目录 时 触发事件,例如 open O_CREAT,mkdir 等

IN_DELETE:在监控列表下 文件或目录 被删除时 触发事件

IN_DELETE_SELF:监控文件或目录 本身被删除时 触发事件,而且,如果一个文件或目录被移到其它地方,比如使用 mv 命令,也会触发该事件,因为 mv 命令本质上是拷贝一份当前文件,然后删除当前文件的操作。此时监控终止,并且将收到一个 IN_IGNORED 事件。

IN_MODIFY:文件 被修改时 触发事件,例如:有写操作(write)或者文件内容被清空(truncate)操作。不过需要注意的是,IN_MODIFY 可能会连续触发多次。

IN_MODIFY_SELF:所监控的文件或目录本身 发生移动时 触发事件

IN_MOVED_FROM:将文件或目录 移除 监控列表 触发事件

IN_MOVED_TO:将文件或目录 移入 监控列表 触发事件

IN_OPEN:文件被打开 触发事件

IN_ALL_EVENTS:监控所有事件

IN_MOVE:IN_MOVED_FROM | IN_MOVED_TO 事件的统称


事件结构

上文描述了inotify支持的事件类型,可以看出来支持的事件类型非常丰富,基本满足了我们对于文件监听的各种诉求。除了上述的事件类型外,在这一小节我们会简单描述一下inotify的event结构,通过事件的数据结构可以看出,从事件中我们可以获取到哪些信息。事件的具体数据结构如下:
mask 标志    描述
IN_IGNORED    监控项为内核或者应用程序移除
IN_ISDIR    被监听的是一个目录的路径
IN_Q_OVERFLOW    事件队列溢出
IN_UNMOUNT    包含对象的文件系统遭到卸载

struct inotify_event {
    int        wd;          //监控描述符号,唯一指代一个监控项目
    uint32_t    mask;    //监控的事件类型
    uint32_t    cookie;    //只有重命名才会使用到该字段
    uint32_t    len;        //下面 name 数组的尺寸
    char        name[];    //当受控目录下的文件有变更时,该字符串会记录发生变更的文件的文件名
};

将监控项在监控列表中登记后,应用程序可以用 read() 从 inotify 的文件描述符中读取事件以判定发生了那些事件。若读取之时还没有发生任何事件,则 read() 会阻塞,直至有事件产生。事件发生后,每次调用 read() 会返回一个缓存区,内含一个或多个如下类型的结构体:
struct inotify_event
{
int      wd;       // 指向发生事件的监控项的文件描述符,该字段值由之前对 inotify_add_watch() 的调用返回。用于区分是哪个监控项触发了该事件
uint32_t mask;     // inotify 事件的一位掩码
uint32_t cookie;   // 唯一的关联 inotify 事件的值
uint32_t len;      // 分配给 name 的字节数
char     name[];   // 标识触发该事件的文件名
};

inotify的事件队列并不是无限大的,因为队列也是需要消耗内核内存的,因此会设置一些上限加以限制,具体的配置可以通过修改对/proc/sys/fs/inotify下的三个文件来达到控制文件事件监听的目的。

max_queued_events: 规定了 inotify事件队列的数量上限,一旦超出这个限制,系统就会生成IN_Q_OVERFLOW事件,该事件上文有详细描述,这里不再赘述。
max_user_instances:对由每个真实用户 ID 创建的inotify实例数的限制值。
max_user_watchers:对由每个真实用户 ID 创建的监控项数量的限制值。

inotify-tools的安装和使用

inotify-tools提供两种工具,一是 inotifywait,它是用来监控文件或目录的变化;二是inotifywatch,它是用来统计文件系统访问的次数。

kernel 2.6.13以上都支持(直接安装使用)运行测试命令
# grep INOTIFY_USER /boot/config-$(uname -r)
CONFIG_INOTIFY_USER=y
如果结果=y(yes)则表示支持


inotifywait:监控文件或目录变化
语法:
inotifywait [-hcmrq] [-e ] [-t ] [--format ] [--timefmt ] [ ... ]

参数:
-h,--help
输出帮助信息
@
排除不需要监视的文件,可以是相对路径,也可以是绝对路径。
--fromfile
从文件读取需要监视的文件或排除的文件,一个文件一行,排除的文件以@开头。
-m, --monitor
接收到一个事情而不退出,无限期地执行。默认的行为是接收到一个事情后立即退出。
-d, --daemon
跟--monitor一样,除了是在后台运行,需要指定--outfile把事情输出到一个文件。也意味着使用了--syslog。
-o, --outfile
输出事情到一个文件而不是标准输出。
-s, --syslog
输出错误信息到系统日志
-r, --recursive
监视一个目录下的所有子目录。
-q, --quiet
指定一次,不会输出详细信息,指定二次,除了致命错误,不会输出任何信息。
--exclude
正则匹配需要排除的文件,大小写敏感。
--excludei
正则匹配需要排除的文件,忽略大小写。
-t , --timeout
设置超时时间,如果为0,则无限期地执行下去。
-e , --event
指定监视的事件。
-c, --csv
输出csv格式。
--timefmt
指定时间格式,用于--format选项中的%T格式。

--format
指定输出格式。
%w 表示发生事件的目录
%f 表示发生事件的文件
%e 表示发生的事件
%Xe 事件以“X”分隔
%T 使用由--timefmt定义的时间格式

inotifywatch:统计文件的系统访问次数

语法:
inotifywatch [-hvzrqf] [-e ] [-t ] [-a ] [-d ] [ ... ]

参数:
-h, --help
输出帮助信息
-v, --verbose
输出详细信息
@
排除不需要监视的文件,可以是相对路径,也可以是绝对路径。
--fromfile
从文件读取需要监视的文件或排除的文件,一个文件一行,排除的文件以@开头。
-z, --zero
输出表格的行和列,即使元素为空
--exclude
正则匹配需要排除的文件,大小写敏感。
--excludei
正则匹配需要排除的文件,忽略大小写。
-r, --recursive
监视一个目录下的所有子目录。
-t , --timeout
设置超时时间
-e , --event
只监听指定的事件。
-a , --ascending
以指定事件升序排列。
-d , --descending
以指定事件降序排列。

可监听事件
access    文件读取。
modify    文件更改。
attrib    文件属性更改,如权限,时间戳等。
close_write    以可写模式打开的文件被关闭,不代表此文件一定已经写入数据。
close_nowrite    以只读模式打开的文件被关闭。
close    文件被关闭,不管它是如何打开的。
open    文件打开。
moved_to    一个文件或目录移动到监听的目录,即使是在同一目录内移动,此事件也触发。
moved_from    一个文件或目录移出监听的目录,即使是在同一目录内移动,此事件也触发。
move    包括moved_to和 moved_from
move_self    文件或目录被移除,之后不再监听此文件或目录。
create    文件或目录创建。
delete    文件或目录删除。
delete_self    文件或目录移除,之后不再监听此文件或目录。
unmount    文件系统取消挂载,之后不再监听此文件系统。

inotify-tools 是一套用户空间的工具,包括 inotifywait 和 inotifywatch,其使用 inotify API。是一种强大的、细粒度的、异步文件系统监控机制,它满足各种各样的文件监控需要,可以监控文件系统的访问属性、读写属性、权限属性、删除创建、移动等操作,也就是可以监控文件发生的一切变化。可以对文件系统事件进行监控,并生成相应的警告或日志。它是由一个C库和一组命令行组成,提供Linux下inotify的简单接口。inotify-tools安装后会得到inotifywait和inotifywatch这两条命令:inotifywait是用来监控文件或目录变化的文件系统事件。inotifywatch是用来统计文件系统访问的次数,监控文件或文件夹的变化,并输出统计信息的。

下面是一些inotifywait应用的例子:

监控目录或文件的创建、删除、移动等操作:
inotifywait -m -r -e create,delete,move /path-of-directory

这个命令将监视/path/to/directory目录中所有文件和目录的创建、删除和移动操作。当有文件或目录被创建、删除或移动时,将显示相应的信息。

监控文件的修改操作:
inotifywait -m -r -e modify /path/to/file

这个命令将监视/path/to/file文件的修改操作。当有文件被修改时,这个命令将显示相应的信息。

监控目录或文件的属性变化:
inotifywait -m -r -e attrib /path/to/directory

这个命令将监视/path/to/directory目录中所有文件和目录的属性变化。当有文件或目录的属性发生变化时,这个命令将显示相应的信息。

监控多个目录或文件的事件:
inotifywait -m -r -e create,delete,move /path/to/directory1 /path/to/directory2 /path/to/file1 /path/to/file2

这个命令将监视多个目录和文件的创建、删除和移动操作。当有目录或文件发生相应的事件时,这个命令将显示相应的信息。

监控事件并执行命令:
inotifywait -m -r -e create,delete,move /path/to/directory -- /path/to/command
inotifywait -mrq --timefmt '%Y%m%d %h:%M' --format '%T %e %w%f' -e modify fdelete ,create $fmnitordir

上面的命令将监视/path/to/directory目录中所有文件和目录的创建、删除和移动操作,并在事件发生时执行指定的命令。可以将命令替换为你想要执行的任何命令。这些例子只是inotifywait的一些基本用法,大家可以根据自己的需求进一步扩展和定制。

一台网络摄像机,用来监控家里的的入口是否有异常,如果有异常就会自动录像。录像文件存放在nas服务器上,为了把文件备份又将录像文件定时复制到远程云服务器上。之前是采用定时备份的模式,实时性不好。为了保证备份的实时性,查找了linux对文件的监控功能的资料,终于发现了inotify-tools这个工具。通过对其学习,编写了一个自动执行脚本,当检测到摄像头保存视频的文件夹有变动,就自动用rsync把更改的文件同步到远程云服务器上。
#!/bin/bash
dir=/ipcam    #指定需要监视的文件夹
log_file=/watch.log        #指定输出信息的文件,方便后面查看
rsync_file=/xxx.sh    #发现文件有变化时需要执行的脚本文件
while
inotifywait -r $dir -o $log_file -e close \    #这里只监视文件的close动作
--timefmt '%d/%m/%y %H:%M' --format '%T %w %f %e';
do  
 bash $rsync_file    #执行同步文件的脚本
 sleep 10m        #等待10分钟,如果不加这个命令,同步会非常频繁,没必要
done


# inotifywatch /ipcam -t 300
Establishing watches...
Finished establishing watches, now collecting statistics.
total  access  close_nowrite  open  filename
51     25      13             13    /ipcam/
这里监控了5分钟时间,总共出现了51次文件变更,其中access 25次,close_nowrite 13次,open 13次,结果清晰明了。


常见问题

1、监控文件时,无法根据 event->name 输出对应更改的文件名

原因:在 linux 手册中关于 inotify 的描述有对应解释。如果是监控目录,此时目录下的文件触发事件,会输出对应的文件名。但是如果只监控文件,则无法输出对应更改的文件名。

2、监控文件时,无法持续监控,第二次更改文件时,它没有响应

原因:这是由于 vim 的工作机制引起的,vim 会先将源文件复制为另一个文件,然后在另一文件基础上编辑(后缀名为 swp),保存的时候再将这个文件覆盖源文件。此时原来的文件已经被后来的新文件代替,因此监视对象所监视的文件已经不存在了,所以自然不会产生任何事件。

解决方法:
重新使用 inotify_add_watch,将该文件加入监控队列。


参考来源:
Linux-man-inotify.7

Linux 使用 inotify 监控文件或目录变化