inotify监控Linux文件系统的必备利器
本文主要向大家介绍了如何使用inotify控制Linux文件系统中的事件。在具体的学习之前我们先来看看什么是inotify以及他的历史简介。我们还会向大家介绍inotify的具体应用情况以及使中遇到问题的解决方案。
inotify 介绍
文件系统事件监控对于从文件管理器到安全工具等多种程序来说都是必要的。自 2.6.13 版本内核开始,Linux 就提供 inotify 功能,这允许监控程序打开一个独立文件描述符,并针对一系列特定的事件来监控一个或多个文件或者目录,例如打开、关闭、移动/重命名、 删除、创建或者改变属性。在以后的版本中还提供更多增强功能,因此在依赖这些特性之前请先检查系统的内核版本。
在本文中,您将学会如何在简单的监控程序当中使用 inotify 函数。下载样例代码 并在系统当中进行编译,来为后面的研究做准备。
历史简介
在 inotify 之前有 dnotify 。不幸的是,dnotify 有局限性,无法满足用户的需求, inotify 的优势如下:
Inotify 采用简单的文件描述符,而 dnotify 需要为每个受监控的目录打开一个文件描述符。 这使得同时监控多个目录成本很高,而且还会遇到每进程文件描述符限制问题。
inotify 所采用的文件描述符通过系统调用获得,并且没有相关的设备或文件。对于 dnotify ,文件描述符与目录关系固定,避免了相关设备未被加载的问题,这是可移动媒体的典型问题。对于 inotify ,如果受监控文件或目录所在的文件系统未被加载, 则会产生一个事件,然后该监控会被自动移除。
Inotify 能够监控文件或目录。Dnotify 监控目录, 因此程序员必须保持 stat 结构或者等效的数据结构,来反映目录当中被监控的文件, 然后在一个事件发生时,将其与当前状态进行对比,来知晓目录当中的条目发生了什么情况。
如前面所述,inotify 采用文件描述符,允许程序员采用标准 select 或者 poll 函数来对事件进行监控。 这允许高效的多重 I/O 或者与 Glib 的 mainloop 集成。相比之下,dnotify 采用信号, 这使得程序员感到难度更大或者不够流畅。 在 2.6.25 版本内核中,inotify 也增加了 Signal-drive I.O 通告功能。
用于 inotify 的 API
Inotify 提供简单的 API ,采用最小的文件描述符并允许细粒度监控。与 inotify 的通信通过系统调用实现。
inotify_init
是用于创建 inotify 实例的系统调用,并返回一个指向该实例的文件描述符。
inotify_init1
与 inotify_init 相似,并带有附加标志。如果这些标志没有指定,将采用与 inotify_init 相同的值。
inotify_add_watch
增加对文件或目录的监控并指定需要监控哪些事件。 标志用于控制是否将事件加入到已有的监控当中,是否只有路径代表一个目录才进行监控, 是否需要追踪符号链接,是否进行一次性监控,当首次出现事件后就停止监控。
inotify_rm_watch
从监控列表中移除监控项目。
read
读取包含一个或多个事件信息的缓存。
close
关闭文件描述符,并移除所有在该描述符上的监控。 当关于某一实例的文件描述符都关闭以后, 资源和下层的对象都将释放,来供内核再次使用。
因此,典型的监控程序要进行如下操作:
利用 inotify_init 打开文件描述符;
增加一个或多个监控;
等待事件;
处理事件,然后返回并等待更多事件;
当没有活跃的监控时或者基于某些信号的指示,关闭文件描述符,清空,然后退出。
在下一部分中,您将看到可以监控的事件,以及它们如何在简单程序当中运行。最后您将看到如何进行事件监控。
通告
当应用程序读取一个通告时,事件的顺序也被读取到缓存中。事件在一个变长结构体中被返回,见清单 1 。如果数据占满了缓存,可能需要对最后一个条目进行局部事件信息或者局部名处理。
清单 1. 用于 inotify 的事件结构体
1.struct inotify_event
2.{
3.int wd; /* Watch descriptor. */
4.uint32_t mask; /* Watch mask. */
5.uint32_t cookie; /* Cookie to synchronize two events. */
6.uint32_t len; /* Length (including NULs) of name. */
7.char name __flexarr; /* Name. */
8.};
注意,只有当监控对象是一个目录并且事件与目录内部相关项目有关,而与目录本身无关时,才提供 name 字段。 如果 IN_MOVED_FROM 事件与相应的 IN_MOVED_TO 事件都与被监控的项目有关,cookie 就可用于将两者关联起来。 事件类型在掩码字段中返回,并伴随着能够被内核设置的标志。 例如,如果事件与目录有关,则标志 IN_ISDIR 将由内核设置。
能够监控的事件
有几种事件能够被监控。有一些,比如 IN_DELETE_SELF 只应用到正在被监控的项目,然而另一些比如 IN_ATTRIB 或者 IN_OPEN 可以应用到监控过的项目, 或者如果该项目是目录,则可以应用到其所包含的目录或文件。
IN_ACCESS
被监控项目或者被监控目录当中的条目被访问过。例如,一个打开的文件被读取。
IN_MODIFY
被监控项目或者被监控目录当中的条目被修改过。例如,一个打开的文件被修改。
IN_ATTRIB
被监控项目或者被监控目录当中条目的元数据被修改过。例如,时间戳或者许可被修改。
IN_CLOSE_WRITE
一个打开的,等待写入的文件或目录被关闭。
IN_CLOSE_NOWRITE
一个以只读方式打开的文件或目录被关闭。
IN_CLOSE
是可以很便捷地对前面提到的两个关闭事件(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)进行逻辑或操作的掩码。
IN_OPEN
文件或目录被打开。
IN_MOVED_FROM
被监控项目或者被监控目录当中的条目被移出监控区域。该事件还包含一个 cookie 来实现 IN_MOVED_FROM 与 IN_MOVED_TO 的关联。
IN_MOVED_TO
文件或目录被移入监控区域。该事件包含一个针对 IN_MOVED_FROM 的 cookie 。如果文件或目录只是被重命名,将能看到这两个事件,如果它只是被移入或移出非监控区域,将只能看到一个事件。 如果移动或重命名一个被监控项目,监控将继续进行。参见下面的 IN_MOVE-SELF 。
IN_MOVE
是可以很便捷地对前面提到的两个移动事件(IN_MOVED_FROM | IN_MOVED_TO)进行逻辑或操作的掩码。
IN_CREATE
在被监控目录当中创建了子目录或文件。
IN_DELETE
被监控目录当中有子目录或文件被删除。
IN_DELETE_SELF
被监控项目本身被删除。监控被终止并收到一个 IN_IGNORED 事件。
IN_MOVE_SELF
监控项目本身被移动。
除了事件标志以外,还可以在 inotify 头文件(/usr/include/sys/inotify.h)中找到其他几个标志。 例如,如果只想监控第一个事件,可以在增加监控时设置 IN_ONESHOT 标志。
inotify 简单应用
这里的简单应用遵循以上的通用逻辑。 我们使用一个信号处理程序来监控 ctrl-c(SIGINT)并且 重置一个标志(keep_running)使应用了解终止操作。 真实的 inotify 调用在 utility 例程当中完成。注意, 我们还创建了一个队列,这样能够将事件从 inotify 底层对象中清除,留着稍后处理。在真实的应用当中,您可能希望用其他(具有更高优先级)的线程来完成这一操作。 这里的列举的应用程序,只是为了对一般原理进行举例说明。我们采用了一个简单的事件链表, 队列中的每一条目都包含原始事件加上用于存储指向队列中下一事件指针的空间。
主程序
清单 2 中展示了信号处理程序和主程序。在本例当中,对所有在命令行中出现过的文件或目录进行监控, 并利用事件掩码 IN_ALL_EVENTS 来监控每一对象的所有事件。 在真实的应用程序中,您可能只希望追踪文件与目录的创建或删除事件,因此您可以掩去打开、关闭以及属性改变事件。 如果您对文件或目录的重命名和移动不感兴趣,您也可以掩去各种移动事件。关于更多细节,参见 inotify 帮助信息。
清单 2. inotify-test.c 的简单主程序
1./* Signal handler that simply resets a flag to cause termination */
2.void signal_handler (int signum)
3.{
4.keep_running = 0;
5.}
6.int main (int argc, char **argv)
7.{
8./* This is the file descriptor for the inotify watch */
9.int inotify_fd;
10.keep_running = 1;
11./* Set a ctrl-c signal handler */
12.if (signal (SIGINT, signal_handler) == SIG_IGN)
13.{
14./* Reset to SIG_IGN (ignore) if that was the prior state */
15.signal (SIGINT, SIG_IGN);
16.}
17./* First we open the inotify dev entry */
18.inotify_fd = open_inotify_fd ();
19.if (inotify_fd > 0)
20.{
21./* We will need a place to enqueue inotify events,
22.this is needed because if you do not read events
23.fast enough, you will miss them. This queue is
24.probably too small if you are monitoring something
25.like a directory with a lot of files and the directory
26.is deleted.
27.*/
28.queue_t q;
29.q = queue_create (128);
30./* This is the watch descriptor returned for each item we are
31.watching. A real application might keep these for some use
32.in the application. This sample only makes sure that none of
33.the watch descriptors is less than 0.
34.*/
35.int wd;
36./* Watch all events (IN_ALL_EVENTS) for the directories and
37.files passed in as arguments.
38.Read the article for why you might want to alter this for
39.more efficient inotify use in your app.
40.*/
41.int index;
42.wd = 0;
43.printf(" ");
44.for (index = 1; (index < argc) && (wd >= 0); index++)
45.{
46.wd = watch_dir (inotify_fd, argv[index], IN_ALL_EVENTS);
47.}
48.if (wd > 0)
49.{
50./* Wait for events and process them until a
51.termination condition is detected
52.*/
53.process_inotify_events (q, inotify_fd);
54.}
55.printf (" Terminating ");
56./* Finish up by closing the fd, destroying the queue,
57.and returning a proper code
58.*/
59.close_inotify_fd (inotify_fd);
60.queue_destroy (q);
61.}
62.return 0;
63.}
利用 inotify_init 打开文件描述符
清单 3 展示了用于创建 inotify 实例的简单应用函数,并为其获得一个文件描述符。文件描述符返回给了调用者。 如果出现错误,返回值将为负。
清单 3. 使用 inotify_init
1./* Create an inotify instance and open a file descriptor
2.to access it */
3.int open_inotify_fd ()
4.{
5.int fd;
6.watched_items = 0;
7.fd = inotify_init ();
8.if (fd < 0)
9.{
10.perror ("inotify_init () = ");
11.}
12.return fd;
13.}
利用 inotify_add_watch 来增加监控
有了用于 inotify 实例的文件描述符之后,就需要增加一个或多个监控。 可以使用掩码来设置想要监控的事件。在本例当中,采用掩码 IN_ALL_EVENTS,来监控全部的有效事件。
清单 4. 使用 inotify_add_watch
1.int watch_dir (int fd, const char *dirname, unsigned long mask)
2.{
3.int wd;
4.wd = inotify_add_watch (fd, dirname, mask);
5.if (wd < 0)
6.{
7.printf ("Cannot add watch for "%s" with event mask %lX", dirname,
8.mask);
9.fflush (stdout);
10.perror (" ");
11.}
12.else
13.{
14.watched_items++;
15.printf ("Watching %s WD=%d ", dirname, wd);
16.printf ("Watching = %d items ", watched_items);
17.}
18.return wd;
19.}
事件处理循环
现在我们已经设置了一些监控,接下来就要等待事件。如果还存在监控,并且 keep_running 标志没有被信号处理程序重置,则循环会一直进行。循环进程等待事件的发生,对有效事件进行排队,并在返回等待状态之前并处理队列。 在真实应用程序当中,可能会采用一个线程将事件放入队列,而采用另一个线程处理它们,清单 5 展示了该循环。
清单 5. 事件处理循环
1.int process_inotify_events (queue_t q, int fd)
2.{
3.while (keep_running && (watched_items > 0))
4.{
5.if (event_check (fd) > 0)
6.{
7.int r;
8.r = read_events (q, fd);
9.if (r < 0)
10.{
11.break;
12.}
13.else
14.{
15.handle_events (q);
16.}
17.}
18.}
19.return 0;
20.}
等待事件
在本程序中,循环会不停地进行下去,直至发生被监控事件或者收到了中断信号。清单 6 展示了相关代码。
清单 6. 等待事件或中断
1.int event_check (int fd)
2.{
3.fd_set rfds;
4.FD_ZERO (&rfds);
5.FD_SET (fd, &rfds);
6./* Wait until an event happens or we get interrupted
7.by a signal that we catch */
8.return select (FD_SETSIZE, &rfds, NULL, NULL, NULL);
9.}
读取事件
当事件发生时,程序会依照缓存区的大小来读取尽量多的事件,然后把这些事件放入队列等待事件处理程序来处理。 样例代码对于事件量大于 16.384-byte 之类的问题不作处理。 想处理这类问题,需要在缓存末端处理局部事件。当前对名字长度所做的限制没有问题,但是优秀的防御式编程会检查名字,来确保不会出现缓存区溢出。
清单 7. 读取事件并排队
1.int read_events (queue_t q, int fd)
2.{
3.char buffer[16384];
4.size_t buffer_i;
5.struct inotify_event *pevent;
6.queue_entry_t event;
7.ssize_t r;
8.size_t event_size, q_event_size;-
9.int count = 0;
10.r = read (fd, buffer, 16384);
11.if (r <= 0)
12.return r;
13.buffer_i = 0;
14.while (buffer_i < r)
15.{
16./* Parse events and queue them. */
17.pevent = (struct inotify_event *) &buffer[buffer_i];
18.event_size = offsetof (struct inotify_event, name) + pevent->
len;
19.q_event_size = offsetof (struct queue_entry, inot_ev.name) +
20.pevent->len;
21.event = malloc (q_event_size);
22.memmove (&(event->inot_ev), pevent, event_size);
23.queue_enqueue (event, q);
24.buffer_i += event_size;
25.count++;
26.}
27.printf (" %d events queued ", count);
28.return count;
29.}
处理事件
最终,需要对事件做处理。本例中的应用程序不处理事件,只报告发生了哪些事件。如果事件结构体中有一个名字出现, 程序将报告其是目录还是文件。在发生移动操作时,还会报告与移动或重命名事件相关的 cookie 信息。 清单 8 展示了部分代码,包括对一些事件的处理。参见 下载 部分可获得全部代码。
清单 8. 处理事件
1.void handle_event (queue_entry_t event)
2.{
3./* If the event was associated with a filename, we will
store it here */
4.char *cur_event_filename = NULL;
5.char *cur_event_file_or_dir = NULL;
6./* This is the watch descriptor the event occurred on */
7.int cur_event_wd = event->inot_ev.wd;
8.int cur_event_cookie = event->inot_ev.cookie;
9.unsigned long flags;
10.if (event->inot_ev.len)
11.{
12.cur_event_filename = event->inot_ev.name;
13.}
14.if ( event->inot_ev.mask & IN_ISDIR )
15.{
16.cur_event_file_or_dir = "Dir";
17.}
18.else
19.{
20.cur_event_file_or_dir = "File";
21.}
22.flags = event->inot_ev.mask &
23.~(IN_ALL_EVENTS | IN_UNMOUNT | IN_Q_OVERFLOW | IN_IGNORED );
24./* Perform event dependent handler routines */
25./* The mask is the magic that tells us what file operation
occurred */
26.switch (event->inot_ev.mask &
- 最新评论
