为什么inotify事件会触发多次?

这个问题源自我在Stackoverflow上提出的另一个问题。我正在使用Watcher - 相同的问题也适用于Incron - 来监视文件夹及其子文件夹的更改,并将这些更改悄悄地保存到Dropbox。

我监视write_close事件 - IN_CLOSE_WRITE - 是为了这个目的。最初,我是在监听modify事件,即IN_MODIFY。虽然这样可以工作,但我发现当写入大文件时,它会触发多次。听起来很合理,所以我切换到了IN_CLOSE_WRITE,因为我觉得对于给定的文件,它只会发生一次。

然而,事实并非如此。即使是一个非常小的文本文件 - 只有一个字符 - 在Nano中创建的事件发生两次。最好的情况是,在Dropbox上同步相同的文件时,这可能导致不必要的流量增加两倍。在我自己的情况下,这会导致灾难,因为在第一次事件中,我进行了同步,然后删除了服务器端的文件。结果是 - 在第二次事件中,Dropbox端的文件变成了0字节。
目前,我通过让我的同步脚本在执行任何其他操作之前休眠10秒钟,并在尝试进行Dropbox同步之前检查相关文件是否仍然存在来解决这个问题。这样做是有效的,因为在第二次迭代中,该文件已经不存在,脚本会直接终止。
这听起来像是一种取巧的方法。或许不算是坏办法,但我更希望能够理解 - 为什么连IN_CLOSE_WRITE事件会多次发生呢?
一些额外的信息
  • 检查以确保没有多个 watcher 运行的实例。

ps ax|grep watcher.py 的输出

23880 ?        Sl     0:01 python /usr/local/bin/watcher.py restart
24977 pts/0    S+     0:00 grep --color=auto watcher.py

文件系统是ext4。我应该提到,我在使用Incron时遇到了完全相同的问题。我通过一个批处理脚本从/etc/rc2.d执行启动Watcher守护进程。而Incron则通过默认的apt-get install incron安装自动启动,无需我进行任何操作。
我的watcher.ini文件的核心内容如下所示。
[DEFAULT]
logfile=/var/log/watcher.log
pidfile=/var/run/watcher.pid

[job1]
watch=/path/to/watch

events=write_close
excluded=
recursive=true
autoadd=true

command=/home/datastore.php $filename

我将datastore.php脚本简化到最基本的程度,以验证它是否被启动了两次,而没有我的混乱的Dropbox上传+源代码删除的代码。
#! /usr/bin/php
<?php
file_put_contents('/tmp/watcher',$argv[1],FILE_APPEND);

?>

然后我在问题路径下创建了一个小文件,然后检查了/tmp/watcher。问题仍然存在-该文件仍然有两个连续的$argv[1]条目。


1我尝试了很多变化,但无法复制您所描述的多个IN_CLOSE_WRITE事件触发的问题。无论我做什么,都只会产生一个inotify输出。我将继续尝试其他方法,但目前只有一些问题需要解决。请问使用的是哪种文件系统?是Ext4还是其他类型? - lornix
@lornix - 请查看我对问题的修改。文件系统是ext4,我相当确定没有两个Watcher实例正在运行。我在使用Incron时也遇到了同样的问题。 - DroidOS
你说过:“我执行同步,然后删除服务器端的文件。” 这个删除操作会触发第二个事件吗?你能否禁用“删除”程序并再试一次? - Germar
@Germar - 看到我问题的编辑了吗?即使同步脚本没有真正的同步和unlink,问题仍然存在。 - DroidOS
抱歉,我已经没有更多的想法了,我无法在我的任何设备上重现这个问题。我只收到了一个事件,没有其他的。似乎有其他的因素参与了,没有提到的东西。你是否安装了杀毒软件?类似的东西有吗? - lornix
@lornix - 谢谢。知道你无法复现问题对我很有帮助。我的设置相当复杂 - Nginx、Redis、Memcached、Incron、Watcher...。我打算从Watcher开始建立一个新的服务器,看看是否能有更好的运气。 - DroidOS
你还在忍受这个吗? - Jacob Vlijm
4个回答

我刚刚使用IN_CLOSE_WRITE完成了这个,并最终找到了问题所在。 我的原始脚本是逐行从stdin读取邮件,然后将每一行写入文件。这导致incron在每次写入行时都会触发。我根据incron事件的数量和文件中的行数来观察到了这一点。
然后,我将原始脚本更改为在写入文件之前先读取所有的stdin。一旦这样做了,我就不再看到同一个文件上发生多次incron执行的情况了。

我没有足够的声望来将此作为评论发布,但您确定临时的、可能隐藏的文件没有被创建吗?我曾经遇到过类似的问题,当编辑时vim会创建一个.swp文件,关闭时会触发事件。它还会捕捉到原始文件的关闭事件。
听起来你注意到同一个文件上触发了多次事件,这是我无法复现的情况——这只会在临时文件和原始文件上各触发一次。
我用nano进行了快速测试,我不认为它会创建任何临时文件(至少对于几个字符的情况),但是您的设置中是否还有其他可能依赖类似行为的东西呢?

谢谢你的建议。我遇到了inotify多个问题,即使我使用Nano创建一个非常简单的1字节文件,或者仅仅将一个字符从控制台重定向到文件中也会出现这个问题。我在原始问题中提出的"解决方案"目前可以让我继续运行。然而,从长远来看,我唯一的解决方案是从头开始重新构建我的服务器,以确定错误是何时开始的 - 我的设置包括Incron、Watcher(顺便说一句,当我只有Incron时发生了这个问题)、MariaDB、Nginx、Redis和Memcached,不算是一个"简单"的配置。 - DroidOS
以防万一,请确认您没有重复监视同一个文件夹。如果没有,例如当我通过OS X的Samba客户端将文件复制到Samba共享时会发生以下情况: create,close_write,delete,create,close_write 当我用Windows客户端进行操作时,看起来更合理: create,write_close,无其他内容。 因此,我通过使用IN_MODIFY、IN_ONESHOT和/directory. sleep someTime命令来监视文件的第一次修改来解决我的问题。 oneshot完成了这个任务。 - Edik Mkoyan

我不确定,但很可能第一个write_close将文件属性写入其中,比如创建时间,然后才写入实际数据。 事实上,rsync会创建一个临时文件,当一切都完成后,它会将临时文件移动到同一文件夹中的实际文件,这样在使用rsync时很容易监视是否已经正常创建,并且移动是一个原子操作。另一方面,在inotify中有一种称为one shot的东西,可能可以在第一次修改消息时触发某些操作,正如你建议的,在开始操作之前睡眠一段合理的时间。我正在研究这个问题,如果有新的发现,我会更新的。 https://superuser.com/questions/1133642/traceing-the-moment-when-file-is-completely-copied-to-samba-share-with-inotify

你说得很有道理。这需要一些调查。谢谢你的提示。如果我发现这确实是个问题,我会回复的。 - DroidOS
我认为ATTRIB对文件本身没有任何作用,我错了。 - Edik Mkoyan

我注意到,如果我使用>cp,我会得到2个事件。
root@vm-dvde05:~# touch file
root@vm-dvde05:~# inotifywait --recursive --monitor --event modify file &
[1] 31835
root@vm-dvde05:~# Setting up watches.  Beware: since -r was given, this may take a while!
Watches established.

root@vm-dvde05:~# echo string>file
file MODIFY
file MODIFY
root@vm-dvde05:~# cat file
string
root@vm-dvde05:~# cp /etc/hosts file
file MODIFY
file MODIFY
root@vm-dvde05:~#

但是如果我使用append >>,它只显示单个事件:
root@vm-dvde05:~# echo string2>>file
file MODIFY
root@vm-dvde05:~# kill %1
root@vm-dvde05:~#
[1]+  Terminated              inotifywait --recursive --monitor --event modify file
root@vm-dvde05:~#

为了监控单个事件,我使用CLOSE_WRITE