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)
			{	
				// 该监控项为目录或目录下的文件时
				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

如下图所示:

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

6.3 运行截图

6.3.1 不加任何参数

此时会提示信息,需要输入两个路径用于监控,如下图所示:

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

6.3.2 监控两个文件

监控 /etc/a,/etc/b

如下图所示:

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

6.3.3 监控两个目录

监控 /etc,/tmp

如下图所示:

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

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

7 inotify 监控文件时常见问题

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

原因:

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

Linux C 使用 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://*.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

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

上一篇:cmd、powerShell、终端或Terminal的区别?简单理解他们之间的关系!


下一篇:分布式缓存Redis/memcache选型