Linux C 使用 inotify 监控文件或目录变化
1 运行环境
- 操作系统:Ubuntu 18
2 inotify 简介
-
inotify 是一个 Linux 内核特性(监视文件系统事件),它用于监控文件系统,比如删除、读、写操作等,当发生对应事件时,则会触发 inotify。当监控目录时,与该目录自身以及该目录下面的文件都会被监控,其上有事件发生时都会通知给应用程序
-
inotify 监控机制为非递归,若想监控整个目录子树内的事件,则需对该树中的每个目录发起 inotify_add_watch() 调用
-
使用 inotify:创建一个文件描述符,附加一个或多个监视器(一个监视器 是一个路径和一组事件),然后使用 read() 方法从描述符获取事件信息。read() 并不会用光整个周期,它在事件发生之前是被阻塞的。
-
因为 inotify 通过传统的文件描述符工作,可使用 select(),poll(),epoll() 以及由信号驱动的 I/O 来监控 inotify 文件描述符
-
要使用 inotify,必须具备一台带有 2.6.13 或更新内核的 Linux 机器(以前的 Linux 内核版本使用更低级的文件监控器 dnotify)。如果您不知道内核的版本,请转到 shell,输入 uname -a
3 inotify API
3.1 inotify_init
创建一个 inotify 实例并返回一个引用 inotify 实例的文件描述符
函数原型:
#include<sys/inotify.h>
int inotify_init(void);
返回值:
-
成功:该函数的返回值为一个文件描述符,该文件描述符所指代的文件中将会保存所监控的 文件/目录 所发生的 事件集。
-
失败:返回 -1,并且将 errno 设置为对应错误。
使用及解释:
int fd = inotify_init();
fd 为所指的 inotify 实例的 监控列表,系统调用 inotify_add_watch() 可以向该 fd 追加 新的监控项。
3.2 inotify_add_watch
针对 fd 所指的 inotify 实例的 监控列表 追加 新的监控项。
函数原型:
#include<sys/inotify.h>
int inotify_add_watch(int fd,const char *pathname,uint32_t mask);
返回值:
-
成功:返回值为一个用于 唯一指代此 监控项 的描述符
-
失败:返回值 < 0 ,则代表添加该监控项失败,需要检测 pathname 是否有可读权限,是否存在,系统的监控队列是否已满等
参数:
-
pathname 为想要创建的监控项所对应的文件,特别注意调用该接口必须要对该文件有读权限,该函数只对文件做一次检查,如果在监控时修改了所监控的文件读权限,则不会影响继续监控此文件
-
mask 为一位掩码,针对 pathname 定义了想要监控的事件,此函数的返回值为一个用于唯一指代此监控项的描述符(将在 4 inotify 事件 中介绍)
4 inotify 常用监控事件
-
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 事件的统称
5 存储 inotify 事件 结构体 struct inotify_event
将 监控项 在 监控列表 中登记后,应用程序可以用 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[]; // 标识触发该事件的文件名
};
注意:
如果是监控目录,此时目录下的文件触发事件,会输出对应的文件名。但是如果只监控文件,则无法根据 event->name 输出对应更改的文件名,原因参考 7.1 监控文件时,无法根据 event->name 输出对应更改的文件名
6 inotify 示例
6.1 代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/inotify.h>
#define EVENT_NUM 12
const char *event_str[EVENT_NUM] =
{
"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"
};
int inotifyTask(char *argv[])
{
int errTimes = 0;
int fd = -1;
INIT_INOTIFY:
fd = inotify_init();
if(fd < 0)
{
fprintf(stderr, "inotify_init failed\n");
printf("Error no.%d: %s\n", errno, strerror(errno));
goto INOTIFY_FAIL;
}
int wd1 = -1;
int wd2 = -1;
struct inotify_event *event;
int length;
int nread;
char buf[BUFSIZ];
int i = 0;
buf[sizeof(buf) - 1] = 0;
INOTIFY_AGAIN:
wd1 = inotify_add_watch(fd, argv[1], IN_ALL_EVENTS);
if(wd1 < 0)
{
fprintf(stderr, "inotify_add_watch %s failed\n", argv[1]);
printf("Error no.%d: %s\n", errno, strerror(errno));
if(errTimes < 3)
{
goto INOTIFY_AGAIN;
}
else
{
goto INOTIFY_FAIL;
}
}
wd2 = inotify_add_watch(fd, argv[2], IN_ALL_EVENTS);
if(wd2 < 0)
{
fprintf(stderr, "inotify_add_watch %s failed\n", argv[2]);
printf("Error no.%d: %s\n", errno, strerror(errno));
if(errTimes < 3)
{
goto INOTIFY_AGAIN;
}
else
{
goto INOTIFY_FAIL;
}
}
length = read(fd, buf, sizeof(buf) - 1);
nread = 0;
// inotify 事件发生时
while(length > 0)
{
printf("\n");
event = (struct inotify_event *)&buf[nread];
// 遍历所有事件
for(i = 0; i< EVENT_NUM; i++)
{
// 判断事件是否发生
if( (event->mask >> i) & 1 )
{
// 该监控项为目录或目录下的文件时
if(event->len > 0)
{
fprintf(stdout, "%s --- %s\n", event->name, event_str[i]);
}
// 该监控项为文件时
else if(event->len == 0)
{
if(event->wd == wd1)
{
fprintf(stdout, "%s --- %s\n", argv[1], event_str[i]);
}
if(event->wd == wd2)
{
fprintf(stdout, "%s --- %s\n", argv[2], event_str[i]);
}
}
}
}
nread = nread + sizeof(struct inotify_event) + event->len;
length = length - sizeof(struct inotify_event) - event->len;
}
goto INOTIFY_AGAIN;
close(fd);
return 0;
INOTIFY_FAIL:
return -1;
}
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s path path\n", argv[0]);
return -1;
}
if(inotifyTask(argv) == -1)
{
return -1;
}
return 0;
}
6.2 编译
编译命令:
gcc inotify.c -o out
如下图所示:
6.3 运行截图
6.3.1 不加任何参数
此时会提示信息,需要输入两个路径用于监控,如下图所示:
6.3.2 监控两个文件
监控 /etc/a,/etc/b
如下图所示:
6.3.3 监控两个目录
监控 /etc,/tmp
如下图所示:
7 inotify 监控文件时常见问题
7.1 监控文件时,无法根据 event->name 输出对应更改的文件名
原因:
在 linux 手册中关于 inotify 的描述有对应解释。 如果是监控目录,此时目录下的文件触发事件,会输出对应的文件名。但是如果只监控文件,则无法输出对应更改的文件名。如下图所示:
7.2 监控文件时,无法持续监控,第二次更改文件时,它没有响应
原因:
这是由于 vim 的工作机制引起的,vim 会先将源文件复制为另一个文件,然后在另一文件基础上编辑(后缀名为 swp),保存的时候再将这个文件覆盖源文件。此时原来的文件已经被后来的新文件代替,因此监视对象所监视的文件已经不存在了,所以自然不会产生任何事件。
解决方法:
重新使用 inotify_add_watch,将该文件加入监控队列。
8 参考资料
1、linux 手册中关于 inotify 的描述 - https://man7.org/linux/man-pages/man7/inotify.7.html
2、Stack Overflow 中关于 inotify inotify_event event->name is empty Shay 的提问 - Will Chappell 的回答- https://stackoverflow.com/questions/7957132/inotify-inotify-event-event-name-is-empty
3、inotify 检测文件被修改 - https://www.169it.com/tech-qa-linux/article-13284431940448660571.html
4、如何在C中使用inotify - http://www.voidcn.com/article/p-ntlqecbe-bwk.html
5、c使用inotify监控linux路径下文件变化 - meccaendless(一江明澈的水)- https://blog.csdn.net/meccaendless/article/details/80238997
6、如何用c语言实现对目录或是文件进行文件的添加,修改,删除监控(inotify) - jenie - https://blog.csdn.net/jenie/article/details/106195668?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-3&spm=1001.2101.3001.4242