本文主要向大家介绍了如何使用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 的事件结构体
structinotify_event { intwd;/*Watchdescriptor.*/ uint32_tmask;/*Watchmask.*/ uint32_tcookie;/*Cookietosynchronizetwoevents.*/ uint32_tlen;/*Length(includingNULs)ofname.*/ charname__flexarr;/*Name.*/ };
注意,只有当监控对象是一个目录并且事件与目录内部相关项目有关,而与目录本身无关时,才提供 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 标志。#p#
inotify 简单应用
这里的简单应用遵循以上的通用逻辑。 我们使用一个信号处理程序来监控 ctrl-c(SIGINT)并且 重置一个标志(keep_running)使应用了解终止操作。 真实的 inotify 调用在 utility 例程当中完成。注意, 我们还创建了一个队列,这样能够将事件从 inotify 底层对象中清除,留着稍后处理。在真实的应用当中,您可能希望用其他(具有更高优先级)的线程来完成这一操作。 这里的列举的应用程序,只是为了对一般原理进行举例说明。我们采用了一个简单的事件链表, 队列中的每一条目都包含原始事件加上用于存储指向队列中下一事件指针的空间。
主程序
清单 2 中展示了信号处理程序和主程序。在本例当中,对所有在命令行中出现过的文件或目录进行监控, 并利用事件掩码 IN_ALL_EVENTS 来监控每一对象的所有事件。 在真实的应用程序中,您可能只希望追踪文件与目录的创建或删除事件,因此您可以掩去打开、关闭以及属性改变事件。 如果您对文件或目录的重命名和移动不感兴趣,您也可以掩去各种移动事件。关于更多细节,参见 inotify 帮助信息。
清单 2. inotify-test.c 的简单主程序
/*Signalhandlerthatsimplyresetsaflagtocausetermination*/ voidsignal_handler(intsignum) { keep_running=0; } intmain(intargc,char**argv) { /*Thisisthefiledescriptorfortheinotifywatch*/ intinotify_fd; keep_running=1; /*Setactrl-csignalhandler*/ if(signal(SIGINT,signal_handler)==SIG_IGN) { /*ResettoSIG_IGN(ignore)ifthatwasthepriorstate*/ signal(SIGINT,SIG_IGN); } /*Firstweopentheinotifydeventry*/ inotify_fd=open_inotify_fd(); if(inotify_fd>0) { /*Wewillneedaplacetoenqueueinotifyevents, thisisneededbecauseifyoudonotreadevents fastenough,youwillmissthem.Thisqueueis probablytoosmallifyouaremonitoringsomething likeadirectorywithalotoffilesandthedirectory isdeleted. */ queue_tq; q=queue_create(128); /*Thisisthewatchdescriptorreturnedforeachitemweare watching.Arealapplicationmightkeeptheseforsomeuse intheapplication.Thissampleonlymakessurethatnoneof thewatchdescriptorsislessthan0. */ intwd; /*Watchallevents(IN_ALL_EVENTS)forthedirectoriesand filespassedinasarguments. Readthearticleforwhyyoumightwanttoalterthisfor moreefficientinotifyuseinyourapp. */ intindex; wd=0; printf("\n"); for(index=1;(index<argc)&&(wd>=0);index++) { wd=watch_dir(inotify_fd,argv[index],IN_ALL_EVENTS); } if(wd>0) { /*Waitforeventsandprocessthemuntila terminationconditionisdetected */ process_inotify_events(q,inotify_fd); } printf("\nTerminating\n"); /*Finishupbyclosingthefd,destroyingthequeue, andreturningapropercode */ close_inotify_fd(inotify_fd); queue_destroy(q); } return0; }
利用 inotify_init 打开文件描述符
清单 3 展示了用于创建 inotify 实例的简单应用函数,并为其获得一个文件描述符。文件描述符返回给了调用者。 如果出现错误,返回值将为负。
清单 3. 使用 inotify_init
/*Createaninotifyinstanceandopenafiledescriptor toaccessit*/ intopen_inotify_fd() { intfd; watched_items=0; fd=inotify_init(); if(fd<0) { perror("inotify_init()="); } returnfd; }
利用 inotify_add_watch 来增加监控
有了用于 inotify 实例的文件描述符之后,就需要增加一个或多个监控。 可以使用掩码来设置想要监控的事件。在本例当中,采用掩码 IN_ALL_EVENTS,来监控全部的有效事件。
清单 4. 使用 inotify_add_watch
intwatch_dir(intfd,constchar*dirname,unsignedlongmask) { intwd; wd=inotify_add_watch(fd,dirname,mask); if(wd<0) { printf("Cannotaddwatchfor\"%s\"witheventmask%lX",dirname, mask); fflush(stdout); perror(""); } else { watched_items++; printf("Watching%sWD=%d\n",dirname,wd); printf("Watching=%ditems\n",watched_items); } returnwd; }
#p#事件处理循环
现在我们已经设置了一些监控,接下来就要等待事件。如果还存在监控,并且 keep_running 标志没有被信号处理程序重置,则循环会一直进行。循环进程等待事件的发生,对有效事件进行排队,并在返回等待状态之前并处理队列。 在真实应用程序当中,可能会采用一个线程将事件放入队列,而采用另一个线程处理它们,清单 5 展示了该循环。
清单 5. 事件处理循环
intprocess_inotify_events(queue_tq,intfd) { while(keep_running&&(watched_items>0)) { if(event_check(fd)>0) { intr; r=read_events(q,fd); if(r<0) { break; } else { handle_events(q); } } } return0; }
等待事件
在本程序中,循环会不停地进行下去,直至发生被监控事件或者收到了中断信号。清单 6 展示了相关代码。
清单 6. 等待事件或中断
intevent_check(intfd) { fd_setrfds; FD_ZERO(&rfds); FD_SET(fd,&rfds); /*Waituntilaneventhappensorwegetinterrupted byasignalthatwecatch*/ returnselect(FD_SETSIZE,&rfds,NULL,NULL,NULL); }
读取事件
当事件发生时,程序会依照缓存区的大小来读取尽量多的事件,然后把这些事件放入队列等待事件处理程序来处理。 样例代码对于事件量大于 16.384-byte 之类的问题不作处理。 想处理这类问题,需要在缓存末端处理局部事件。当前对名字长度所做的限制没有问题,但是优秀的防御式编程会检查名字,来确保不会出现缓存区溢出。
清单 7. 读取事件并排队
intread_events(queue_tq,intfd) { charbuffer[16384]; size_tbuffer_i; structinotify_event*pevent; queue_entry_tevent; ssize_tr; size_tevent_size,q_event_size;- intcount=0; r=read(fd,buffer,16384); if(r<=0) returnr; buffer_i=0; while(buffer_i<r) { /*Parseeventsandqueuethem.*/ pevent=(structinotify_event*)&buffer[buffer_i]; event_size=offsetof(structinotify_event,name)+pevent-> len; q_event_size=offsetof(structqueue_entry,inot_ev.name)+ pevent->len; event=malloc(q_event_size); memmove(&(event->inot_ev),pevent,event_size); queue_enqueue(event,q); buffer_i+=event_size; count++; } printf("\n%deventsqueued\n",count); returncount; }
处理事件
最终,需要对事件做处理。本例中的应用程序不处理事件,只报告发生了哪些事件。如果事件结构体中有一个名字出现, 程序将报告其是目录还是文件。在发生移动操作时,还会报告与移动或重命名事件相关的 cookie 信息。 清单 8 展示了部分代码,包括对一些事件的处理。参见 下载 部分可获得全部代码。
清单 8. 处理事件
voidhandle_event(queue_entry_tevent) { /*Iftheeventwasassociatedwithafilename,wewill storeithere*/ char*cur_event_filename=NULL; char*cur_event_file_or_dir=NULL; /*Thisisthewatchdescriptortheeventoccurredon*/ intcur_event_wd=event->inot_ev.wd; intcur_event_cookie=event->inot_ev.cookie; unsignedlongflags; if(event->inot_ev.len) { cur_event_filename=event->inot_ev.name; } if(event->inot_ev.mask&IN_ISDIR) { cur_event_file_or_dir="Dir"; } else { cur_event_file_or_dir="File"; } flags=event->inot_ev.mask& ~(IN_ALL_EVENTS|IN_UNMOUNT|IN_Q_OVERFLOW|IN_IGNORED); /*Performeventdependenthandlerroutines*/ /*Themaskisthemagicthattellsuswhatfileoperation occurred*/ switch(event->inot_ev.mask& (IN_ALL_EVENTS|IN_UNMOUNT|IN_Q_OVERFLOW|IN_IGNORED)) { /*Filewasaccessed*/ caseIN_ACCESS: printf("ACCESS:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*Filewasmodified*/ caseIN_MODIFY: printf("MODIFY:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*Filechangedattributes*/ caseIN_ATTRIB: printf("ATTRIB:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*Fileopenforwritingwasclosed*/ caseIN_CLOSE_WRITE: printf("CLOSE_WRITE:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*Fileopenread-onlywasclosed*/ caseIN_CLOSE_NOWRITE: printf("CLOSE_NOWRITE:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*Filewasopened*/ caseIN_OPEN: printf("OPEN:%s\"%s\"onWD#%i\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd); break; /*FilewasmovedfromX*/ caseIN_MOVED_FROM: printf("MOVED_FROM:%s\"%s\"onWD#%i.Cookie=%d\n", cur_event_file_or_dir,cur_event_filename,cur_event_wd, cur_event_cookie); break; . .(othercases) . /*Watchwasremovedexplicitlybyinotify_rm_watchor automatically becausefilewasdeleted,orfilesystemwasunmounted.*/ caseIN_IGNORED: watched_items--; printf("IGNORED:WD#%d\n",cur_event_wd); printf("Watching=%ditems\n",watched_items); break; /*Someunknownmessagereceived*/ default: printf("UNKNOWNEVENT\"%X\"OCCURREDforfile\"%s\"onWD#%i\n", event->inot_ev.mask,cur_event_filename,cur_event_wd); break; } /*IfanyflagsweresetotherthanIN_ISDIR,reporttheflags*/ if(flags&(~IN_ISDIR)) { flags=event->inot_ev.mask; printf("Flags=%lX\n",flags); } }
该例子用来举例说明 inotify 如何工作以及能够监控哪些事件。 您的实际需求将决定对哪些事件进行监控以及如何处理这些事件。#p#
用法举例
在这部分当中,我们创建一个简单的两级目录结构,目录中有一个文件, 然后运行简单程序来举例说明 inotify 所能监控的一些事件。 我们将在一个终端会话中启动 inotify 样例程序, 但是因为他是在后台运行的(利用 &)因此程序的输出与我们输入的命令会交替出现。 应该在一个终端窗口运行该程序,而在其他窗口输入命令。清单 9 展示了简单文件结构和空文件的创建,以及最初启动该简单程序时的输出。
清单 9. 创建样例环境
ian@attic4:~/inotify-sample$mkdir-pdir1/dir2 ian@attic4:~/inotify-sample$touchdir1/dir2/file1 ian@attic4:~/inotify-sample$./inotify_testdir1/ dir1/dir2/dir1/dir2/file1& [2]8733 ian@attic4:~/inotify-sample$ Watchingdir1/WD=1 Watching=1items Watchingdir1/dir2/WD=2 Watching=2items Watchingdir1/dir2/file1WD=3 Watching=3items ian@attic4:~/inotify-sample$
在清单 10 当中,展示了列举 dir2 内容时的输出。 第一个事件报告与 dir1 有关,显示出有些东西,名字叫做 dir2 ,在与监控描述符 1 相关的被监控目录当中被打开了。 第二个条目与监控描述符 2 有关,显示出被监控项目(在本例中为 dir2 )被打开了。如果正在监控目录树中的多个项目, 可能会经常遇到这种双重输出。
清单 10. 列出 dir2 的内容
ian@attic4:~/inotify-sample$lsdir1/dir2 file1 4eventsqueued OPEN:Dir"dir2"onWD#1 OPEN:Dir"(null)"onWD#2 CLOSE_NOWRITE:Dir"dir2"onWD#1 CLOSE_NOWRITE:Dir"(null)"onWD#2
在清单 11 当中,在 file1 中加入一些文字。需要再次强调对于文件以及该文件所在目录的双重打开,关闭,和修改事件。还要注意不是全部事件都会被立刻读取。排队例程被调用了3次,每次有两个事件。如果再次运行该程序,并且每次操作相同,您未必会再次遇到这一特别情况。
清单 11. 在 file1 中加入一些文字
ian@attic4:~/inotify-sample$echo"Sometext">>dir1/dir2/file1 2eventsqueued OPEN:File"file1"onWD#2 OPEN:File"(null)"onWD#3 2eventsqueued MODIFY:File"file1"onWD#2 MODIFY:File"(null)"onWD#3 2eventsqueued CLOSE_WRITE:File"file1"onWD#2 CLOSE_WRITE:File"(null)"onWD#3
在清单 12 当中,改变 file1 的属性。我们再次得到有关被监控项目以及其所在目录的双重输出。
清单 12. 改变文件属性
ian@attic4:~/inotify-sample$chmoda+wdir1/dir2/file1 2eventsqueued ATTRIB:File"file1"onWD#2 ATTRIB:File"(null)"onWD#3
现在将文件 file1 移动到上一级目录 dir1 当中。在清单 13 中显示了输出结果。 这次没有双重条目。我们实际上得到了 3 个条目,每个目录一个,文件本身一个。 注意 cookie (569) 允许将 MOVED-FROM 事件与 MOVED_TO 事件关联起来。
清单 13. 将文件 file1 移入 dir1
ian@attic4:~/inotify-sample$mvdir1/dir2/file1dir1 3eventsqueued MOVED_FROM:File"file1"onWD#2.Cookie=569 MOVED_TO:File"file1"onWD#1.Cookie=569 MOVE_SELF:File"(null)"onWD#3
现在创建一个 file1 到 file2 的硬链接。由于到 inode 的链接数量变了,我们得到针对 file1 的 ATTRIB 事件,还得到一个针对 file2 的 CREATE 事件。
清单 14. 创建硬链接
ian@attic4:~/inotify-sample$lndir1/file1dir1/file2 2eventsqueued ATTRIB:File"(null)"onWD#3 CREATE:File"file2"onWD#1
现在将文件 file1 移入当前目录,将其重命名为 file3 。当前目录没有被监控,因此不存在与 MOVED_FROM 事件相关联的 MOVED_TO 事件。
清单 15. 将 file1 移入不受监控的目录当中
ian@attic4:~/inotify-sample$mvdir1/file1./file3 2eventsqueued MOVED_FROM:File"file1"onWD#1.Cookie=572 MOVE_SELF:File"(null)"onWD#3
此时,dir2 是空的,因此可以移动它。注意我们得到一个关于监控描述符 2 的 IGNORED 事件,可见我们只在监控两个项目。
清单 16. 移动 dir2
ian@attic4:~/inotify-sample$rmdirdir1/dir2 3eventsqueued DELETE:Dir"dir2"onWD#1 DELETE_SELF:File"(null)"onWD#2 IGNORED:WD#2 Watching=2items
移动文件 file3。注意这次我们没有得到 IGNORED 事件。为什么呢?为什么得到了关于 file 3 的 ATTRIB 事件(就是原来的 dir1/dir2/file1)?
清单 17. 删除 file3
ian@attic4:~/inotify-sample$rmfile3 1eventsqueued ATTRIB:File"(null)"onWD#3
记住我们创建了 file1 到 file2 的硬链接。清单 17 显示我们还在通过监控描述符 3 来监控 file2,尽管最开始不存在文件 2!
清单 18. 仍对 file2 进行监控!
ian@attic4:~/inotify-sample$touchdir1/file2 6eventsqueued OPEN:File"file2"onWD#1 OPEN:File"(null)"onWD#3 ATTRIB:File"file2"onWD#1 ATTRIB:File"(null)"onWD#3 CLOSE_WRITE:File"file2"onWD#1 CLOSE_WRITE:File"(null)"onWD#3
现在删除 dir1 来监控事件级联,因为程序它不监控任何事,它结束了自己。
清单 19. 删除 dir1
ian@attic4:~/inotify-sample$rm-rfdir1 8eventsqueued OPEN:Dir"(null)"onWD#1 ATTRIB:File"(null)"onWD#3 DELETE_SELF:File"(null)"onWD#3 IGNORED:WD#3 Watching=1items DELETE:File"file2"onWD#1 CLOSE_NOWRITE:Dir"(null)"onWD#1 DELETE_SELF:File"(null)"onWD#1 IGNORED:WD#1 Watching=0items Terminating
inotify 的使用情况
可将 inotify 用于多种目标。下面列举一些可能的情况:
性能监控
您可能想确定应用程序打开最频繁的文件是哪个。如果发现一个小文件被频繁打开与关闭, 您可能会考虑采用内存版,或者改变应用程序来采取其他方式共享该数据。
元信息
您可能想记录文件的附加信息,例如起始创建时间,或者最后改变该文件的用户 id 。
安全
您可能会因为安全原因,需要对特定文件或目录的所有访问进行监控。
我们的样例代码监控所有事件并进行报告。实际上,您可能想依据您的需要,来查看这些事件的特定子集。您可能想监控不同被监控项目的不同事件。例如,您可能想监控文件的打开与关闭事件,但对于目录只想监控创建与删除事件。在任何可能的时候,您可以监控您所感兴趣的最小事件集。
结束语
在应用到性能监控,程序调试,以及自动化等领域时,inotify 是功能强大,高级粒度机制的 Linux 文件系统监控工具。利用本文提供的样例代码,您可以开始编写用来实时记录文件系统事件并最小化性能开销的应用程序。