Inotify: 高效、实时的Linux文件系统事件监控框架

概要 - 为什么需要监控文件系统?

在日常工作中,人们往往需要知道在某些文件(夹)上都有那些变化,比如:

  • 通知配置文件的改变
  • 跟踪某些关键的系统文件的变化
  • 监控某个分区磁盘的整体使用情况
  • 系统崩溃时进行自动清理
  • 自动触发备份进程
  • 向服务器上传文件结束时发出通知

通常使用文件轮询的通知机制,但是这种机制只适用于经常改变的文件(因为它可以确保每过x秒就可以得到i/o),其他情况下都非常低效,并且有时候会丢失某些类型的变化,例如文件的修改时间没有改变。像Tripwire这样的数据完整性系统,它们基于时间调度来跟踪文件变化,但是如果想实时监控文件的变化的话,那么时间调度就束手无策了。Inotify就这样应运而生了。本文将简要介绍inotify,告诉我们如何监控文件夹,如何一有变化就报告相关消息事件,并介绍了一些相关工具, 我们可以把它们添加到自己的工具箱中。

Inotify到底是什么?

Inotify是一种文件变化通知机制,Linux内核从2.6.13开始引入。在BSD和Mac OS系统中比较有名的是kqueue,它可以高效地实时跟踪Linux文件系统的变化。近些年来,以fsnotify作为后端,几乎所有的主流Linux发行版都支持Inotify机制。如何知道你的Linux内核是否支持Inotify机制呢?很简单,执行下面这条命令:

% grep INOTIFY_USER /boot/config-$(uname -r)
CONFIG_INOTIFY_USER=y

如果输出('CONFIG_INOTIFY_USER=y'),那么你可以马上享受Inotify之旅了。

简单的文件变化通知样例:

好的开始是成功的一半,对于了解Inotify机制来说,让我们从使用inotifywait程序开始,它包含在inotify-tools工具包中。假如我们打算监控/srv/test文件夹上的操作,只需执行:

% inotifywait -rme modify,attrib,move,close_write,create,delete,delete_self /srv/test
Setting up watches.  Beware: since -r was given, this may take a while!
Watches established.

上述任务运行的同时,我们在另一个shell里依次执行以下操作:创建文件夹,然后在新文件夹下创建文件,接着删除新创建的文件:

% mkdir /srv/test/infoq
% echo TODO > /srv/test/infoq/article.txt
% rm /srv/test/infoq/article.txt

在运行inotifywait的shell中将会打印以下信息:

/srv/test/ CREATE,ISDIR infoq
/srv/test/infoq/ CREATE article.txt
/srv/test/infoq/ MODIFY article.txt
/srv/test/infoq/ CLOSE_WRITE,CLOSE article.txt
/srv/test/infoq/ DELETE article.txt

显而易见,只要有变化我们就会收到相关的通知。如果想了解关于Inotify提供的事件(如modify, atrrib等)的详细信息,请参考inotifywatch的manpage。实际使用时,如果并不想监控某个大文件夹,那么就可以使用inotifywait的exclude选项。例如:我们要忽略文件夹/srv/test/large,那么就可以这样来建立监控:

% inotifywait --exclude '^/srv/test/(large|ignore)/' -rme modify,attrib,move,close_write,create,delete,delete_self /srv/test
Setting up watches.  Beware: since -r was given, this may take a while!
Watches established.

上面的例子中,在exclude选项的匹配串中我们使用了正则表达式,因为我们不想将名称中含有large或ignore的文件也排除掉。我们可以测试一下:

% echo test > /srv/test/action.txt
% echo test > /srv/test/large/no_action.txt
% echo test > /srv/test/ignore/no_action.txt
% echo test > /srv/test/large-name-but-action.txt

这里inotifywait应该只会报告'action.txt'和'large-name-but-action.txt'两个文件的变化,而忽略子文件夹'large'和'ignore'下的文件,结果也确实如此;

/srv/test/ CREATE action.txt
/srv/test/ MODIFY action.txt
/srv/test/ CLOSE_WRITE,CLOSE action.txt
/srv/test/ CREATE large-name-but-action.txt
/srv/test/ MODIFY large-name-but-action.txt
/srv/test/ CLOSE_WRITE,CLOSE large-name-but-action.txt

另外,通过使用-t选项我们还可以定义inotifywait的监控时间,既可以让它执行一段时间,也可以让它一直运行。util-linux-ng的logger命令也可以实现此功能,不过得先把相关的消息事件发送到syslog,然后从syslog服务器再分析整合。

Inotifywatch - 使用inotify来统计文件系统访问信息

Inotify-tools中还有一个工具叫inotifywatch,它会先监听文件系统的消息事件,然后统计每个被监听文件或文件夹的消息事件,之后输出统计信息。比如我们想知道某个文件夹上有那些操作:

% inotifywatch -v -e access -e modify -t 120 -r ~/InfoQ
Establishing watches...
Setting up watch(es) on /home/mika/InfoQ
OK, /home/mika/InfoQ is now being watched.
Total of 58 watches.
Finished establishing watches, now collecting statistics.
Will listen for events for 120 seconds.
total  modify  filename
2      2       /home/mika/InfoQ/inotify/

很显然,这里我们监控的是~/InfoQ文件夹,并且可以看到/home/mika/InfoQ/inotify上发生了两个事件。方法虽简单,但却很有效。

Inotify的配置选项

使用Inotify时,要特别注意内核中关于它的两个配置。首先/proc/sys/fs/inotify/max_user_instances 规定了每个用户所能创建的Inotify实例的上限;其次/proc/sys/fs/inotify/max_user_watches规定了每个Inotify实例最多能关联几个监控(watch)。你可以很容易地试验在运行过程中达到上限,如:

% inotifywait -r /
Setting up watches.  Beware: since -r was given, this may take a while!
Failed to watch /; upper limit on inotify watches reached!
Please increase the amount of inotify watches allowed per user via `/proc/sys/fs/inotify/max_user_watches'.

如果要改变这些配置,只要向相应的文件写入新值即可,如下所示:

# cat /proc/sys/fs/inotify/max_user_watches
8192
# echo 16000 > /proc/sys/fs/inotify/max_user_watches
# cat /proc/sys/fs/inotify/max_user_watches
16000

使用Inotify的一些工具

近一段时间出现了很多基于Inotify的超炫的工具,如incron,它是一个类似于cron的守护进程(daemon),传统的cron守护进程都是在规定的某个时间段内执行,而incron由于使用了Inotify,可以由事件触发执行。同时incron的安装简单而直观,比如在debian上,首先在/etc/incron.allow中添加使用incron的用户(debian默认不允许用户使用incron,因为如果incron使用不慎的话,例如形成死循环,则会导致系统宕机): 

# echo username > /etc/incron.allow

然后调用”incrontab -e“, 在弹出的编辑器中插入我们自己的规则,例如下面的这条简单的规则,文件一变化incron就会发邮件通知我们:

/srv/test/ IN_CLOSE_WRITE mail -s "$@/$#\n" root

从现在开始,一旦/src/test文件夹中的文件被修改,就会发送一封邮件。但是注意不要让incron监控整个子目录树,因为Inotify只关注inodes,而不在乎是文件还是文件夹,所以基于Inotify的软件都需要自己来处理/预防递归问题。关于incontab详细使用,请参考incrontab的manpage。

如果你还要处理incoming文件夹,那么你可能需要inoticoming。当有文件进入incoming文件夹时Inoticoming就会执行某些动作,从而可以用inoticoming来管理debian的软件仓库(例如软件仓库中一旦有上传源码包或是新添加的二进制包就立刻自动进行编译),另外,还可以用它来监控系统是否有新上传文件,如果有就发送通知。类似的工具还有(它们都各有专长):inosync(基于消息通知机制的文件夹同步服务),iwatch(基于Inotify的程序,对文件系统进行实时监控),以及lsyncd(一个守护进程(daemon),使用rsync同步本地文件夹)。

Inotify甚至对传统的Unix工具也进行了改进,例如tail。使用inotail,同时加上-f选项,就可以取代每秒轮询文件的做法。此外,GNU 的coreutils从版本7.5开始也支持Inotify了,我们可以运行下面的命令来确认:

# strace -e inotify_init,inotify_add_watch tail -f ~log/syslog
[...]
inotify_init()                          = 4
inotify_add_watch(4, "/var/log/syslog", IN_MODIFY|IN_ATTRIB|IN_DELETE_SELF|IN_MOVE_SELF) = 1

从现在开始,通过轮询来确实文件是否需要重新读取的方法应该作为古董了。

在脚本中使用Inotify

Inotify机制并不局限于工具,在脚本语言中也完全可以享受Inotify的乐趣,如Python中可以使用pyinotifyinotifyx,Perl中有Filesys-Notify-SimpleLinux-Inotify2,Inotify的Ruby版有ruby-inotifyrb-inotyfssm

总结

综上所述,Inotify为Linux提供了一套高效监控和跟踪文件变化的机制,它可以实时地处理、调试以及监控文件变化,而轮询是一种延迟机制。对于系统管理员,关于实现事件驱动的服务如系统备份,构建服务以及基于文件操作的程序调试等,Inotify无疑提供了强大的支持。

查看英文原文:Inotify: Efficient, Real-Time Linux File System Event Monitoring


感谢侯伯薇对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

Android文件监控FileObserver介绍

在前面介绍了Linux对文件变更监控过程。Android系统在此基础上封装了一个FileObserver类来方便使用Inotify机制。FileObserver是一个抽象类,需要定义子类实现该类的onEvent抽象方法,当被监控的文件或者目录发生变更事件时,将回调FileObserver的onEvent()函数来处理文件或目录的变更事件。

事件监控过程

在FileObserver类中定义了一个静态内部类ObserverThread,该线程类才是真正实现文件或目录监控过程。各种类型的FileObserver都拥有一个ObserverThread实例:

frameworks\base\core\java\android\os\FileObserver.java

01. public abstract class FileObserver {
02. //可监控的事件类型
03. public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE
04. | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE | DELETE_SELF | MOVE_SELF;
05. //静态创建并启动一个文件监控线程
06. private static ObserverThread s_observerThread;
07. static {
08. s_observerThread = new ObserverThread();
09. s_observerThread.start();
10. }
11. // instance
12. private String m_path;
13. private Integer m_descriptor;
14. private int m_mask;
15. }

FileObserver类通过静态方式构造了一个ObserverThread对象:

1. public ObserverThread() {
2. super("FileObserver");
3. m_fd = init();//初始化一个inotify实例,Observer线程就是对该inotify实例进行监控
4. }

frameworks\base\core\jni\android_util_FileObserver.cpp

1. static jint android_os_fileobserver_init(JNIEnv* env, jobject object)
2. {
3. #ifdef HAVE_INOTIFY
4. return (jint)inotify_init();//初始化一个inotify实例  
5. #else // HAVE_INOTIFY
6. return -1;
7. #endif // HAVE_INOTIFY
8. }

inotify_init()函数实现在Linux文件系统Inotify机制 有详细介绍,然后启动ObserverThread线程,ObserverThread线程运行体:

frameworks\base\core\java\android\os\FileObserve$ObserverThread

1. public void run() {
2. observe(m_fd);//监控inotify实例句柄
3. }

frameworks\base\core\jni\android_util_FileObserver.cpp

01. static void android_os_fileobserver_observe(JNIEnv* env, jobject object, jint fd)
02. {
03. #ifdef HAVE_INOTIFY
04. char event_buf[512];//定义事件数组
05. struct inotify_event* event;
06. while (1)
07. {
08. int event_pos = 0;
09. //从inotify实例句柄中读取事件
10. int num_bytes = read(fd, event_buf, sizeof(event_buf));
11. if (num_bytes < (int)sizeof(*event))
12. {
13. if (errno == EINTR)
14. continue;
15. ALOGE("***** ERROR! android_os_fileobserver_observe() got a short event!");
16. return;
17. }
18. //循环处理读取到的事件
19. while (num_bytes >= (int)sizeof(*event))
20. {
21. int event_size;
22. event = (struct inotify_event *)(event_buf + event_pos);
23. jstring path = NULL;
24. if (event->len > 0)
25. {
26. path = env->NewStringUTF(event->name);
27. }
28. //调用ObserverThread的onEvent函数通知上层响应
29. env->CallVoidMethod(object, method_onEvent, event->wd, event->mask, path);
30. if (env->ExceptionCheck()) {
31. env->ExceptionDescribe();
32. env->ExceptionClear();
33. }
34. if (path != NULL)
35. {
36. env->DeleteLocalRef(path);
37. }
38. event_size = sizeof(*event) + event->len;
39. num_bytes -= event_size;
40. event_pos += event_size;
41. }
42. }
43. #endif // HAVE_INOTIFY
44. }

ObserverThread线程循环从inotify实例句柄中读取事件,然后回调ObserverThread的onEvent函数来处理事件。

frameworks\base\core\java\android\os\FileObserve$ObserverThread

01. public void onEvent(int wfd, int mask, String path) {
02. // look up our observer, fixing up the map if necessary...
03. FileObserver observer = null;
04. synchronized (m_observers) {
05. //根据wfd句柄从m_observers表中查找出注册的FileObserver对象
06. WeakReference weak = m_observers.get(wfd);
07. if (weak != null) {  // can happen with lots of events from a dead wfd
08. observer = (FileObserver) weak.get();
09. if (observer == null) {
10. m_observers.remove(wfd);
11. }
12. }
13. }
14. // ...then call out to the observer without the sync lock held
15. if (observer != null) {
16. try {
17. //调用对应的FileObserver对象的onEvent函数来处理事件
18. observer.onEvent(mask, path);
19. catch (Throwable throwable) {
20. Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable);
21. }
22. }
23. }
注册监控watch

FileObserver类提供了startWatching()函数来启动文件监控

frameworks\base\core\java\android\os\FileObserver.java

1. public void startWatching() {
2. if (m_descriptor < 0) {
3. m_descriptor = s_observerThread.startWatching(m_path, m_mask, this);
4. }
5. }

由ObserverThread线程对象启动监控

frameworks\base\core\java\android\os\FileObserver$ObserverThread

01. public int startWatching(String path, int mask, FileObserver observer) {
02. //在Inotify实例中添加一个watch对象,并得到一个watch对象句柄
03. int wfd = startWatching(m_fd, path, mask);
04. Integer i = new Integer(wfd);
05. if (wfd >= 0) {
06. //将watch对象句柄和响应该watch事件的FileObserver以键值对的形式保存在m_observers成员变量中
07. synchronized (m_observers) {
08. m_observers.put(i, new WeakReference(observer));
09. }
10. }
11. return i;
12. }

ObserverThread又调用native方法android_os_fileobserver_startWatching()来添加一个watch

frameworks\base\core\jni\android_util_FileObserver.cpp

01. static jint android_os_fileobserver_startWatching(JNIEnv* env, jobject object, jint fd, jstring pathString, jint mask)
02. {
03. int res = -1;
04. #ifdef HAVE_INOTIFY
05. if (fd >= 0)
06. {
07. const char* path = env->GetStringUTFChars(pathString, NULL);
08. //在Inotify实例上添加一个watch对象
09. res = inotify_add_watch(fd, path, mask);
10. env->ReleaseStringUTFChars(pathString, path);
11. }
12. #endif // HAVE_INOTIFY
13. return res;
14. }
注销监控watch

FileObserver类提供了使用stopWatching()函数来停止文件监控。
frameworks\base\core\java\android\os\FileObserver$ObserverThread

1. public void stopWatching() {
2. if (m_descriptor >= 0) {
3. s_observerThread.stopWatching(m_descriptor);
4. m_descriptor = -1;
5. }
6. }

frameworks\base\core\java\android\os\FileObserve$ObserverThread

1. public void stopWatching(int descriptor) {
2. stopWatching(m_fd, descriptor);
3. }

frameworks\base\core\jni\android_util_FileObserver.cpp

1. static void android_os_fileobserver_stopWatching(JNIEnv* env, jobject object, jint fd, jint wfd)
2. {
3. #ifdef HAVE_INOTIFY
4. inotify_rm_watch((int)fd, (uint32_t)wfd);
5. #endif // HAVE_INOTIFY
6. }

Android文件监控FileObserver介绍 - 深入剖析Android系统 - 博客频道 - CSDN.NET
http://blog.csdn.net/yangwen123/article/details/36379763

Logo

更多推荐