Linux INotif机制详解及实例代码
LinuxINotif机制
一、前言:
众所周知,Linux桌面系统与MAC或Windows相比有许多不如人意的地方,为了改善这种状况,开源社区提出用户态需要内核提供一些机制,以便用户态能够及时地得知内核或底层硬件设备发生了什么,从而能够更好地管理设备,给用户提供更好的服务,如hotplug、udev和inotify就是这种需求催生的。Hotplug是一种内核向用户态应用通报关于热插拔设备一些事件发生的机制,桌面系统能够利用它对设备进行有效的管理,udev动态地维护/dev下的设备文件,inotify是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知,该机制是著名的桌面搜索引擎项目beagle引入的,并在Gamin等项目中被应用。
事实上,在inotify之前已经存在一种类似的机制叫dnotify,但是它存在许多缺陷:
1.对于想监视的每一个目录,用户都需要打开一个文件描述符,因此如果需要监视的目录较多,将导致打开许多文件描述符,特别是,如果被监视目录在移动介质上(如光盘和USB盘),将导致无法umount这些文件系统,因为使用dnotify的应用打开的文件描述符在使用该文件系统。
2.dnotify是基于目录的,它只能得到目录变化事件,当然在目录内的文件的变化会影响到其所在目录从而引发目录变化事件,但是要想通过目录事件来得知哪个文件变化,需要缓存许多stat结构的数据。
3.Dnotify的接口非常不友好,它使用signal。
Inotify是为替代dnotify而设计的,它克服了dnotify的缺陷,提供了更好用的,简洁而强大的文件变化通知机制:
1.Inotify不需要对被监视的目标打开文件描述符,而且如果被监视目标在可移动介质上,那么在umount该介质上的文件系统后,被监视目标对应的watch将被自动删除,并且会产生一个umount事件。
2.Inotify既可以监视文件,也可以监视目录。
3.Inotify使用系统调用而非SIGIO来通知文件系统事件。
4.Inotify使用文件描述符作为接口,因而可以使用通常的文件I/O操作select和poll来监视文件系统的变化。
Inotify可以监视的文件系统事件包括:
IN_ACCESS,即文件被访问
IN_MODIFY,文件被write
IN_ATTRIB,文件属性被修改,如chmod、chown、touch等
IN_CLOSE_WRITE,可写文件被close
IN_CLOSE_NOWRITE,不可写文件被close
IN_OPEN,文件被open
IN_MOVED_FROM,文件被移走,如mv
IN_MOVED_TO,文件被移来,如mv、cp
IN_CREATE,创建新文件
IN_DELETE,文件被删除,如rm
IN_DELETE_SELF,自删除,即一个可执行文件在执行时删除自己
IN_MOVE_SELF,自移动,即一个可执行文件在执行时移动自己
IN_UNMOUNT,宿主文件系统被umount
IN_CLOSE,文件被关闭,等同于(IN_CLOSE_WRITE|IN_CLOSE_NOWRITE)
IN_MOVE,文件被移动,等同于(IN_MOVED_FROM|IN_MOVED_TO)
注:上面所说的文件也包括目录。
二、用户接口
在用户态,inotify通过三个系统调用和在返回的文件描述符上的文件I/操作来使用,使用inotify的第一步是创建inotify实例:
intfd=inotify_init();
每一个inotify实例对应一个独立的排序的队列。
文件系统的变化事件被称做watches的一个对象管理,每一个watch是一个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的inotify事件,每一个位对应一个inotify事件。Watch对象通过watch描述符引用,watches通过文件或目录的路径名来添加。目录watches将返回在该目录下的所有文件上面发生的事件。
下面函数用于添加一个watch:
intwd=inotify_add_watch(fd,path,mask);
fd是inotify_init()返回的文件描述符,path是被监视的目标的路径名(即文件名或目录名),mask是事件掩码,在头文件linux/inotify.h中定义了每一位代表的事件。可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify事件。Wd是watch描述符。
下面的函数用于删除一个watch:
intret=inotify_rm_watch(fd,wd);
fd是inotify_init()返回的文件描述符,wd是inotify_add_watch()返回的watch描述符。Ret是函数的返回值。
文件事件用一个inotify_event结构表示,它通过由inotify_init()返回的文件描述符使用通常文件读取函数read来获得
structinotify_event{ __s32wd;/*watchdescriptor*/ __u32mask;/*watchmask*/ __u32cookie;/*cookietosynchronizetwoevents*/ __u32len;/*length(includingnulls)ofname*/ charname[0];/*stubforpossiblename*/ };
结构中的wd为被监视目标的watch描述符,mask为事件掩码,len为name字符串的长度,name为被监视目标的路径名,该结构的name字段为一个桩,它只是为了用户方面引用文件名,文件名是变长的,它实际紧跟在该结构的后面,文件名将被0填充以使下一个事件结构能够4字节对齐。注意,len也把填充字节数统计在内。
通过read调用可以一次获得多个事件,只要提供的buf足够大。
size_tlen=read(fd,buf,BUF_LEN);
buf是一个inotify_event结构的数组指针,BUF_LEN指定要读取的总长度,buf大小至少要不小于BUF_LEN,该调用返回的事件数取决于BUF_LEN以及事件中文件名的长度。Len为实际读去的字节数,即获得的事件的总长度。
可以在函数inotify_init()返回的文件描述符fd上使用select()或poll(),也可以在fd上使用ioctl命令FIONREAD来得到当前队列的长度。close(fd)将删除所有添加到fd中的watch并做必要的清理。
intinotify_init(void); intinotify_add_watch(intfd,constchar*path,__u32mask); intinotify_rm_watch(intfd,__u32mask);
三、内核实现原理
在内核中,每一个inotify实例对应一个inotify_device结构:
structinotify_device{ wait_queue_head_twq;/*waitqueuefori/o*/ structidridr;/*idrmappingwd->watch*/ structsemaphoresem;/*protectsthisbadboy*/ structlist_headevents;/*listofqueuedevents*/ structlist_headwatches;/*listofwatches*/ atomic_tcount;/*referencecount*/ structuser_struct*user;/*userwhoopenedthisdev*/ unsignedintqueue_size;/*sizeofthequeue(bytes)*/ unsignedintevent_count;/*numberofpendingevents*/ unsignedintmax_events;/*maximumnumberofevents*/ u32last_wd;/*thelastwdallocated*/ };
wq是等待队列,被read调用阻塞的进程将挂在该等待队列上,idr用于把watch描述符映射到对应的inotify_watch,sem用于同步对该结构的访问,events为该inotify实例上发生的事件的列表,被该inotify实例监视的所有事件在发生后都将插入到这个列表,watches是给inotify实例监视的watch列表,inotify_add_watch将把新添加的watch插入到该列表,count是引用计数,user用于描述创建该inotify实例的用户,queue_size表示该inotify实例的事件队列的字节数,event_count是events列表的事件数,max_events为最大允许的事件数,last_wd是上次分配的watch描述符。
每一个watch对应一个inotify_watch结构:
structinotify_watch{ structlist_headd_list;/*entryininotify_device'slist*/ structlist_headi_list;/*entryininode'slist*/ atomic_tcount;/*referencecount*/ structinotify_device*dev;/*associateddevice*/ structinode*inode;/*associatedinode*/ s32wd;/*watchdescriptor*/ u32mask;/*eventmaskforthiswatch*/ };
d_list指向所有inotify_device组成的列表的,i_list指向所有被监视inode组成的列表,count是引用计数,dev指向该watch所在的inotify实例对应的inotify_device结构,inode指向该watch要监视的inode,wd是分配给该watch的描述符,mask是该watch的事件掩码,表示它对哪些文件系统事件感兴趣。
结构inotify_device在用户态调用inotify_init()时创建,当关闭inotify_init()返回的文件描述符时将被释放。结构inotify_watch在用户态调用inotify_add_watch()时创建,在用户态调用inotify_rm_watch()或close(fd)时被释放。
无论是目录还是文件,在内核中都对应一个inode结构,inotify系统在inode结构中增加了两个字段:
#ifdefCONFIG_INOTIFY structlist_headinotify_watches;/*watchesonthisinode*/ structsemaphoreinotify_sem;/*protectsthewatcheslist*/ #endif
inotify_watches是在被监视目标上的watch列表,每当用户调用inotify_add_watch()时,内核就为添加的watch创建一个inotify_watch结构,并把它插入到被监视目标对应的inode的inotify_watches列表。inotify_sem用于同步对inotify_watches列表的访问。当文件系统发生第一部分提到的事件之一时,相应的文件系统代码将显示调用fsnotify_*来把相应的事件报告给
inotify系统,其中*号就是相应的事件名,目前实现包括:
fsnotify_move,文件从一个目录移动到另一个目录
fsnotify_nameremove,文件从目录中删除
fsnotify_inoderemove,自删除
fsnotify_create,创建新文件
fsnotify_mkdir,创建新目录
fsnotify_access,文件被读
fsnotify_modify,文件被写
fsnotify_open,文件被打开
fsnotify_close,文件被关闭
fsnotify_xattr,文件的扩展属性被修改
fsnotify_change,文件被修改或原数据被修改
有一个例外情况,就是inotify_unmount_inodes,它会在文件系统被umount时调用来通知umount事件给inotify系统。
以上提到的通知函数最后都调用inotify_inode_queue_event(inotify_unmount_inodes直接调用inotify_dev_queue_event),该函数首先判断对应的inode是否被监视,这通过查看inotify_watches列表是否为空来实现,如果发现inode没有被监视,什么也不做,立刻返回,反之,遍历inotify_watches列表,看是否当前的文件操作事件被某个watch监视,如果是,调用inotify_dev_queue_event,否则,返回。函数inotify_dev_queue_event首先判断该事件是否是上一个事件的重复,如果是就丢弃该事件并返回,否则,它判断是否inotify实例即inotify_device的事件队列是否溢出,如果溢出,产生一个溢出事件,否则产生一个当前的文件操作事件,这些事件通过kernel_event构建,kernel_event将创建一个inotify_kernel_event结构,然后把该结构插入到对应的inotify_device的events事件列表,然后唤醒等待在inotify_device结构中的wq指向的等待队列。想监视文件系统事件的用户态进程在inotify实例(即inotify_init()返回的文件描述符)上调用read时但没有事件时就挂在等待队列wq上。
四、使用示例
下面是一个使用inotify来监视文件系统事件的例子:
#include<linux/unistd.h> #include<linux/inotify.h> #include<errno.h> _syscall0(int,inotify_init) _syscall3(int,inotify_add_watch,int,fd,constchar*,path,__u32,mask) _syscall2(int,inotify_rm_watch,int,fd,__u32,mask) char*monitored_files[]={ "./tmp_file", "./tmp_dir", "/mnt/sda3/windows_file" }; structwd_name{ intwd; char*name; }; #defineWD_NUM3 structwd_namewd_array[WD_NUM]; char*event_array[]={ "Filewasaccessed", "Filewasmodified", "Fileattributeswerechanged", "writtablefileclosed", "Unwrittablefileclosed", "Filewasopened", "FilewasmovedfromX", "FilewasmovedtoY", "Subfilewascreated", "Subfilewasdeleted", "Selfwasdeleted", "Selfwasmoved", "", "Backingfswasunmounted", "Eventqueuedoverflowed", "Filewasignored" }; #defineEVENT_NUM16 #defineMAX_BUF_SIZE1024 intmain(void) { intfd; intwd; charbuffer[1024]; char*offset=NULL; structinotify_event*event; intlen,tmp_len; charstrbuf[16]; inti=0; fd=inotify_init(); if(fd<0){ printf("Failtoinitializeinotify.\n"); exit(-1); } for(i=0;i<WD_NUM;i++){ wd_array[i].name=monitored_files[i]; wd=inotify_add_watch(fd,wd_array[i].name,IN_ALL_EVENTS); if(wd<0){ printf("Can'taddwatchfor%s.\n",wd_array[i].name); exit(-1); } wd_array[i].wd=wd; } while(len=read(fd,buffer,MAX_BUF_SIZE)){ offset=buffer; printf("Someeventhappens,len=%d.\n",len); event=(structinotify_event*)buffer; while(((char*)event-buffer)<len){ if(event->mask&IN_ISDIR){ memcpy(strbuf,"Direcotory",11); } else{ memcpy(strbuf,"File",5); } printf("Objecttype:%s\n",strbuf); for(i=0;i<WD_NUM;i++){ if(event->wd!=wd_array[i].wd)continue; printf("Objectname:%s\n",wd_array[i].name); break; } printf("Eventmask:%08X\n",event->mask); for(i=0;i<EVENT_NUM;i++){ if(event_array[i][0]=='\0')continue; if(event->mask&(1<<i)){ printf("Event:%s\n",event_array[i]); } } tmp_len=sizeof(structinotify_event)+event->len; event=(structinotify_event*)(offset+tmp_len); offset+=tmp_len; } } }
该程序将监视发生在当前目录下的文件tmp_file与当前目录下的目录tmp_dir上的所有文件系统事件,同时它也将监视发生在文件/mnt/sda3/windows_file上的文件系统事件,注意,/mnt/sda3是SATA硬盘分区3的挂接点。
细心的读者可能注意到,该程序首部使用_syscallN来声明inotify系统调用,原因是这些系统调用是在最新的稳定内核2.6.13中引入的,glibc并没有实现这些系统调用的库函数版本,因此,为了能在程序中使用这些系统调用,必须通过_syscallN来声明这些新的系统,其中的N为要声明的系统调用实际的参数数。还有需要注意的地方是系统的头文件必须与被启动的内核匹配,为了让上面的程序能够成功编译,必须让2.6.13的内核头文件(包括include/linux/*,include/asm/*和include/asm-generic/*)在头文件搜索路径内,并且是第一优先搜索的头文件路径,因为_syscallN需要用到这些头文件中的linux/unistd.h和asm/unistd.h,它们包含了inotify的三个系统调用的系统调用号__NR_inotify_init、__NR_inotify_add_watch和__NR_inotify_rm_watch。
因此,要想成功编译此程序,只要把用户编译好的内核的头文件拷贝到该程序所在的路径,并使用如下命令编译即可:
$gcc-oinotify_example-I.inotify_example.c
注意:当前目录下应当包含linux、asm和asm-generic三个已编译好的2.6.13内核的有文件目录,asm是一个链接,因此拷贝asm头文件的时候需要拷贝asm与asm-ARCH(对于x86平台应当是asm-i386)。然后,为了运行该程序,需要在当前目录下创建文件tmp_file和目录tmp_dir,对于/mnt/sda3/windows_file文件,用户需要依自己的实际情况而定,可能是/mnt/dosc/windows_file,即/mnt/dosc是一个FAT32的windows硬盘,因此用户在编译该程序时需要根据自己的实际情况来修改/mnt/sda3。Windows_file是在被mount硬盘上创建的一个文件,为了运行该程序,它必须被创建。
以下是作者在redhat9.0上运行此程序得到的一些结果:
当运行此程序的时候在另一个虚拟终端执行cat./tmp_file,此程序的输出为:
Someeventhappens,len=48. Objecttype:File Objectname:./tmp_file Eventmask:00000020 Event:Filewasopened Objecttype:File Objectname:./tmp_file Eventmask:00000001 Event:Filewasaccessed Objecttype:File Objectname:./tmp_file Eventmask:00000010 Event:Unwrittablefileclosed
以上事件清楚地说明了cat指令执行了文件open和close操作,当然open和close操作都属于access操作,任何对文件的操作都是access操作。
此外,运行vi./tmp_file,发现vi实际在编辑文件时复制了一个副本,在未保存之前是对副本进行操作。运行vi./tmp_file,修改并保存退出时,发现vi实际在保存修改时删除了最初的文件并把那个副本文件名更改为最初的文件的名称。注意,事件"Filewasignored"表示系统把该文件对应的watch从inotify实例的watch列表中删除,因为文件已经被删除。读者可以自己分别执行命令:echo"abc">./tmp_file、rm-ftmp_file、lstmp_dir、cdtmp_dir;touchc.txt、rmc.txt、umount/mnt/sda3(实际用户需要使用自己当时的mount点路径名),然后分析一下结果。Umount触发两个事件,一个表示文件已经被删除或不在存在,另一个表示该文件的watch被从watch列表中删除。
感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!