Inotify 是文件系统事件监控机制,计划包含在即将发布的 Linux 内核中作为 dnotify 的有效替代。dnotify 是较早内核支持的文件监控机制。Inotify一种强大的、细粒度的、异步的机制,它满足各种各样的文件监控需要,不仅限于安全和性能。下面让我们一起学习如何安装 inotify 和如何构建一个示例用户空间应用程序来响应文件系统事件。
文件系统事件监控对于从文件管理器到安全工具的各种程序都是必要的,但是 dnotify(早期内核中的标准)存在一些局限性,这使我们期待出现一种更加完善的机制。抱着这种期待,我们发现了 inotify,一种更加现代化的文件系统事件监控替代品。
使用 inotify 取代 dnotify 的原因有很多。第一个原因是,dnotify 需要您为每个打算监控是否发生改变的目录打开一个文件描述符。当同时监控多个目录时,这会消耗大量的资源,因为有可能达到每个进程的文件描述符限制。
除此之外,文件描述符会锁定目录,不允许卸载(unmount)支持的设备,这在存在可移动介质的环境中会引发问题。在使用 inotify 时,如果正在监控被卸载的文件系统上的文件,那么监控会被自动移除并且您会接收到一个卸载事件。
dnotify 不如 inotify 的第二个原因是 dnotify 有点复杂。注意,使用 dnotify 基础设施的简单文件系统监控粒度只停留于目录级别。为了使用 dnotify 进行更细粒度的监控,应用程序编程人员必须为每个受监控的目录保留一个 stat
结构的缓存。该用户空间的 stat
结构缓存需要用来明确确定当接收到通知信号时目录发生了什么变化。当获得通知信号时,生成 stat
结构列表并与最新的状态相比较。显而易见,这种技术是不理想的。
inotify 的另一个优点是它使用文件描述符作为基本接口,使应用程序开发者使用 select
和 poll
来监控设备。这允许有效的多路 I/O 和与 Glib 的 mainloop
的集成。相反,dnotify 所使用的信号常常使程序员头疼并且感觉不太优雅。
inotify 通过提供一个更优雅的 API 解决了这些问题,该 API 使用最少的文件描述符,并确保更细粒度的监控。与 inotify 的通信是通过设备节点提供的。基于以上原因,对于监控 Linux 2.6 平台上的文件,inotify 是您最明智的选择。
![]() ![]() |
![]()
|
安装 inotify 的第一步是确定您使用的 Linux 内核是否支持它。检查发行版的最简单方法是,寻找是否存在 /dev/inotify 设备。如果存在该设备,您可以跳到 在简单应用程序中使用 inotify 一节。
在撰写本文时,inotify 包含在 Andrew Morton 的 Linux 2.6-mm 目录树中,而且一些 Linux 发行版正在提供支持 inotify 的内核(包括 Gentoo 和 Ubuntu)或者具有提供支持的补充内核包(例如 Fedora 和 SuSE)。因为 Andrew 可能会根据需要从目录树删除对 inotify 的支持,并且 inotify 版本还处于频繁的开发阶段,所以强烈建议您从头开始打补丁。
如果缺少该设备,您可能需要对内核打补丁并创建该设备。
可以从 Linux Kernel Archives 获得 inotify 补丁(请参阅 参考资料 一节的链接)。
您应该为特定的内核应用最高版本编号的补丁。每个发行版处理内核的安装都有所不同,但以下介绍的是一个通用指导。注意:从 Linux Kernel Archives 获取发行版 2.6 Linux 内核源文件,如果合适,请获取最新的稳定版本。
从进入内核源文件目录开始:
bash:~$ cd /usr/src
因为您早先安装了内核源文件,现在需要将它解压缩:
bash:~$ sudo tar jxvf linux-source-2.6.8.1.tar.bz2
现在,使您的 symlink 指向新的源文件目录树:
bash:~$ sudo ln -sf linux-source-2.6.8.1 linux
改变当前目录到刚才创建的内核源文件目录:
bash:~$ cd linux
拷贝 inotify 补丁:
bash:~$ sudo cp ~/inotify* /usr/src
将内核打补丁:
bash:~$ sudo patch -p1 < ../inotify*.patch
构建内核:
bash:~$ sudo make menuconfig
像平时一样配置您的内核,确保 inotify 工作正常。如果必要,请将新内核添加到引导加载程序中,但是一定要记住维护旧内核的映像和引导加载程序选项。这一步对于不同引导加载程序有所不同(请参阅 参考资料 了解关于特定引导加载程序的更多信息)。重新引导计算机并选择启用 inotify 的新内核。在继续往下操作前,测试您的新内核以确保它工作正常。
接下来,您需要确保创建 /dev/inotify 设备。以下步骤带领您完成这个过程。重要注意:次设备编号可能会发生改变,所以您需要多加注意以确保它随时更新!如果 Linux 安装支持 udev 功能,它将会自动保持更新。
在重新引导到新内核后,您必须获取次设备编号:
bash:~$ dmesg | grep ^inotify
返回结果示例如下:
inotify device minor=63
因为 inotify 是 misc
设备,所以主设备编号是 10。要创建设备节点作为根用户,请执行以下命令:
bash:~$ mknod /dev/inotify c 10 63
注意:如有必要,请使用合适的次设备编号替换“63”。
您可以随意设置您想要的权限。一个示例权限设置如下所示:
bash:~$ chown root:root /dev/inotify
bash:~$ chmod 666 /dev/inotify
现在准备使用 inotify 设备进行文件系统监控。
![]() ![]() |
![]()
|
为演示 inotify 的使用,我将展示如何为文件系统事件构造一个监控任意目录(或单个文件)的示例程序。我将站在一个较高的层次上来展示 inotify 使文件系统监控变得多么容易。
这个简单的示例向我们展示 inotify 在任意目录上设置监控是多么容易。稍后我们将看到主要的帮助器例程。您可以在本文的 下载 一节获取这些例子中使用的示例代码。
/* This program will take as argument a directory name and monitor it, printing event notifications to the console. */ int main (int argc, char **argv) { /* This is the file descriptor for the inotify device */ int inotify_fd; /* First we open the inotify dev entry */ inotify_fd = open_inotify_dev(); if (inotify_fd < 0) { return 0; } /* We will need a place to enqueue inotify events, this is needed because if you do not read events fast enough, you will miss them. */ queue_t q; q = queue_create (128); /* Watch the directory passed in as argument Read on for why you might want to alter this for more efficient inotify use in your app. */ watch_dir (inotify_fd, argv[1], ALL_MASK); process_inotify_events (q, inotify_fd); /* Finish up by destroying the queue, closing the fd, and returning a proper code */ queue_destroy (q); close_inotify_dev (inotify_fd); return 0; } |
以下是每个基于 inotify 的应用程序共同的最重要的帮助器例程:
- 为读取而打开 inotify 设备。
- 对从该设备读取的事件进行排队。
- 允许应用程序对事件通知进行有用处理的实际的每事件处理器。
我不会深入钻研事件排队的细节,因为我们能够使用一些策略来避免排队。提供的代码中就展示了一个这样的方法;更先进的多线程方法可以并且已经在其他地方实现。在那些实现中,读者线程简单地在 inotify 设备上执行 select()
,然后将事件拷贝到一些线程共享的存储空间(或者一些像 Glib 的异步消息队列的东西),以后处理器线程会处理这里的事件。
/* This simply opens the inotify node in dev (read only) */ int open_inotify_dev () { int fd; fd = open("/dev/inotify", O_RDONLY); if (fd < 0) { perror ("open(\"/dev/inotify\", O_RDONLY) = "); } return fd; } |
这对任何一个在 Linux 系统上进行过文件编程的人来说都应该是熟悉的。
/* This method does the dirty work of determining what happened, then allows us to act appropriately */ void handle_event (struct inotify_event *event) { /* If the event was associated with a filename, we will store it here */ char * cur_event_filename = NULL; /* This is the watch descriptor the event occurred on */ int cur_event_wd = event->wd; if (event->len) { cur_event_filename = event->filename; } printf("FILENAME=%s\n", cur_event_filename); printf("\n"); /* Perform event dependent handler routines */ /* The mask is the magic that tells us what file operation occurred */ switch (event->mask) { /* File was accessed */ case IN_ACCESS: printf("ACCESS EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was modified */ case IN_MODIFY: printf("MODIFY EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File changed attributes */ case IN_ATTRIB: printf("ATTRIB EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was closed */ case IN_CLOSE: printf("CLOSE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was opened */ case IN_OPEN: printf("OPEN EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was moved from X */ case IN_MOVED_FROM: printf("MOVE_FROM EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was moved to X */ case IN_MOVED_TO: printf("MOVE_TO EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Subdir was deleted */ case IN_DELETE_SUBDIR: printf("DELETE_SUBDIR EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was deleted */ case IN_DELETE_FILE: printf("DELETE_FILE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Subdir was created */ case IN_CREATE_SUBDIR: printf("CREATE_SUBDIR EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* File was created */ case IN_CREATE_FILE: printf("CREATE_FILE EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Watched entry was deleted */ case IN_DELETE_SELF: printf("DELETE_SELF EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Backing FS was unmounted */ case IN_UNMOUNT: printf("UNMOUNT EVENT OCCURRED: File \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; /* Too many FS events were received without reading them some event notifications were potentially lost. */ case IN_Q_OVERFLOW: printf("Warning: AN OVERFLOW EVENT OCCURRED: \n"); break; case IN_IGNORED: printf("IGNORED EVENT OCCURRED: \n"); break; /* Some unknown message received */ default: printf ("UNKNOWN EVENT OCCURRED for file \"%s\" on WD #%i\n", cur_event_filename, cur_event_wd); break; } } |
在每一条 case
语句中,您可以随意执行任意已实现并且满足需要的方法。
至于性能监控,您可以确定哪些文件是最经常被读取的和它们打开的持续时间。这种监控非常方便,因为在某些情况下,如果文件在短时间内被应用程序重复地读取,它会将文件缓存在内存中而不用返回磁盘去读取,从而提高性能。
很容易举出一些执行有趣操作的特定于事件的处理器的例子。比如,如果您是在为底层文件系统实现一个元数据存储索引,您可能会寻找文件创建事件,不久还会在该文件上触发一个元数据挖掘操作。在安全环境中,如果文件被写入一个无人可以写入的目录,您会触发某些形式的系统警报。
请注意,inotify 支持许多非常细粒度的事件 —— 例如 CLOSE
与 CLOSE_WRITE
。
本文中的代码所列举的许多事件,可能您并不希望在每次代码运行时都看到。实际上,只要可能,您可以并且应该只请求对您的应用程序有用的事件子集。出于测试目的,本文章提供的代码通过严格使用完整掩码(如可下载的示例代码[请参阅 参考资料] 中 main
方法的第 51 行附近或者上面的 清单 1 中的第 29 行所执行的)展示了许多事件。应用程序员通常想要有更多选择,而您则需要更特定的掩码来满足您的需要。这使您可以从上述的 handle_event()
方法中的 catch 语句删除不感兴趣的条目。
![]() ![]() |
当应用于性能监控、调试和自动化领域时,inotify 是一种用于监控 Linux 文件系统的、强大且细粒度的机制。使用本文提供的代码,您就可以编写能够以最低的性能开销响应或记录文件系统事件的应用程序。