inotify -- Linux 2.6内核中的文件系统变化通知机制
众所周知,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实例:
int fd = inotify_init ();
每一个inotify实例对应一个独立的排序的队列。
文件系统的变化事件被称做watches的一个对象管理,每一个watch是一个二元组(目标,事件掩码),目标可以是文件或目录,事件掩码表示应用希望关注的inotify事件,每一个位对应一个inotify事件。Watch对象通过watch描述符引用,watches通过文件或目录的路径名来添加。目录watches将返回在该目录下的所有文件上面发生的事件。
下面函数用于添加一个watch:
int wd = inotify_add_watch (fd, path, mask);
fd是inotify_init()返回的文件描述符,path是被监视的目标的路径名(即文件名或目录名),mask是事件掩码,在头文件linux/inotify.h中定义了每一位代表的事件。可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify事件。Wd是watch描述符。
下面的函数用于删除一个watch:
int ret = inotify_rm_watch (fd, wd);
fd是inotify_init()返回的文件描述符,wd是inotify_add_watch()返回的watch描述符。Ret是函数的返回值。
文件事件用一个inotify_event结构表示,它通过由inotify_init()返回的文件描述符使用通常文件读取函数read来获得
:
struct inotify_event {
__s32wd;/* watch descriptor */
__u32mask;/* watch mask */
__u32cookie;/* cookie to synchronize two events */
__u32len;/* length (including nulls) of name */
charname[0];/* stub for possible name */
};
结构中的wd为被监视目标的watch描述符,mask为事件掩码,len为name字符串的长度,name为被监视目标的路径名,该结构的name字段为一个桩,它只是为了用户方面引用文件名,文件名是变长的,它实际紧跟在该结构的后面,文件名将被0填充以使下一个事件结构能够4字节对齐。注意,len也把填充字节数统计在内。
通过read调用可以一次获得多个事件,只要提供的buf足够大。
size_t len = 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并做必要的清理。
int inotify_init (void);
int inotify_add_watch (int fd, const char *path, __u32 mask);
int inotify_rm_watch (int fd, __u32 mask);
在内核中,每一个inotify实例对应一个inotify_device结构:
struct inotify_device {
wait_queue_head_twq;/* wait queue for i/o */
struct idridr;/* idr mapping wd -> watch */
struct semaphoresem;/* protects this bad boy */
struct list_headevents;/* list of queued events */
struct list_headwatches;/* list of watches */
atomic_tcount;/* reference count */
struct user_struct*user;/* user who opened this dev */
unsigned intqueue_size;/* size of the queue (bytes) */
unsigned intevent_count;/* number of pending events */
unsigned intmax_events;/* maximum number of events */
u32last_wd;/* the last wd allocated */
};
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结构:
struct inotify_watch {
struct list_headd_list; /* entry in inotify_device's list */
struct list_headi_list; /* entry in inode's list */
atomic_tcount;/* reference count */
struct inotify_device*dev;/* associated device */
struct inode*inode; /* associated inode */
s32wd;/* watch descriptor */
u32mask;/* event mask for this watch */
};
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结构中增加了两个字段:
#ifdef CONFIG_INOTIFY
struct list_headinotify_watches; /* watches on this inode */
struct semaphoreinotify_sem;/* protects the watches list */
#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
#include
#include
#include
#include
_syscall0(int, inotify_init)
_syscall3(int, inotify_add_watch, int, fd, const char *, path, __u32, mask)
_syscall2(int, inotify_rm_watch, int, fd, __u32, mask)
char * monitored_files[] = {
"./tmp_file",
"./tmp_dir",
"/mnt/sda3/windows_file"
};
struct wd_name {
int wd;
char * name;
};
#define WD_NUM 3
struct wd_name wd_array[WD_NUM];
char * event_array[] = {
"File was accessed",
"File was modified",
"File attributes were changed",
"writtable file closed",
"Unwrittable file closed",
"File was opened",
"File was moved from X",
"File was moved to Y",
"Subfile was created",
"Subfile was deleted",
"Self was deleted",
"Self was moved",
"",
"Backing fs was unmounted",
"Event queued overflowed",
"File was ignored"
};
#define EVENT_NUM 16
#define MAX_BUF_SIZE 1024
int main(void)
{
int fd;
int wd;
char buffer[1024];
char * offset = NULL;
struct inotify_event * event;
int len, tmp_len;
char strbuf[16];
int i = 0;
fd = inotify_init();
if (fd < 0) {
printf("Fail to initialize inotify.\n");
exit(-1);
}
for (i=0; 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't add watch for %s.\n", wd_array[i].name);
exit(-1);
}
wd_array[i].wd = wd;
}
while(len = read(fd, buffer, MAX_BUF_SIZE)) {
offset = buffer;
printf("Some event happens, len = %d.\n", len);
event = (struct inotify_event *)buffer;
while (((char *)event - buffer) < len) {
if (event->mask & IN_ISDIR) {
memcpy(strbuf, "Direcotory", 11);
}
else {
memcpy(strbuf, "File", 5);
}
printf("Object type: %s\n", strbuf);
for (i=0; i
if (event->wd != wd_array[i].wd) continue;
printf("Object name: %s\n", wd_array[i].name);
break;
}
printf("Event mask: %08X\n", event->mask);
for (i=0; i
if (event_array[i][0] == '\0') continue;
if (event->mask & (1<
printf("Event: %s\n", event_array[i]);
}
}
tmp_len = sizeof(struct inotify_event) + event->len;
event = (struct inotify_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 -o inotify_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硬盘上创建的一个文件,为了运行该程序,它必须被创建。
以下是作者在redhat 9.0上运行此程序得到的一些结果:
当运行此程序的时候在另一个虚拟终端执行cat ./tmp_file,此程序的输出为:
Some event happens, len = 48.
Object type: File
Object name: ./tmp_file
Event mask: 00000020
Event: File was opened
Object type: File
Object name: ./tmp_file
Event mask: 00000001
Event: File was accessed
Object type: File
Object name: ./tmp_file
Event mask: 00000010
Event: Unwrittable file closed
以上事件清楚地说明了cat指令执行了文件open和close操作,当然open和close操作都属于access操作,任何对文件的操作都是access操作。
此外,运行vi ./tmp_file,发现vi实际在编辑文件时复制了一个副本,在未保存之前是对副本进行操作。运行vi ./tmp_file,修改并保存退出时,发现vi实际在保存修改时删除了最初的文件并把那个副本文件名更改为最初的文件的名称。注意,事件"File was ignored"表示系统把该文件对应的watch从inotify实例的watch列表中删除,因为文件已经被删除。读者可以自己分别执行命令:echo "abc" > ./tmp_file、rm -f tmp_file、ls tmp_dir、cd tmp_dir;touch c.txt、rm c.txt、umount /mnt/sda3(实际用户需要使用自己当时的mount点路径名),然后分析一下结果。Umount触发两个事件,一个表示文件已经被删除或不在存在,另一个表示该文件的watch被从watch列表中删除。
beagle是GNOME的桌面搜索引擎项目,inotify的引入就是完全受它的驱动而做的。对于桌面搜索引擎,它一般作为一个优先级很低的后台进程运行,只有在系统没有其他任务可运行时才被调度执行,桌面搜索引擎的主要用途就是为系统的文件系统的文件建立索引数据库,以便用户在需要某文件但又想不起存放在哪里时能够根据某些关键字或特征快速地搜索到需要的文件,就象使用网络搜索引擎google一样便捷。文件系统有个特点就是只有某些文件会变化,因此桌面搜索引擎在第一次建立完索引数据库后,没必要重复遍历所有的文件建立新的索引,它只需要更新修改了的文件的索引,建立新增加的文件的索引,删除已经删除的文件的索引就足够了,这样桌面搜索引擎需要做的工作就大大地减少。Inotify就是为这一意图专门设计的,beagle为需要监视的目录或文件创建了inotify实例,然后它就等待该inotify上发生文件系统事件,如果没有任何文件变化,beagle将不需要任何开销,只有在有被监视的事件发生时,beagle才被唤醒并根据实际事件来更新对应的文件的索引,然后继续睡眠等待下一个文件系统事件发生。在SuSe 9.3和即将发布的10.0中就包含了该桌面搜索引擎,它能够为文档、email、音乐、图象和应用等建立索引。使用过windows下的桌面搜索引擎的读者对google和yahoo以及Microsoft的桌面搜索引擎有深刻的体会,感兴趣读者可以安装SuSe使用一下。
inotify是在2.6.13中引入的新功能,它为用户态监视文件系统的变化提供了强大的支持,本文详尽地介绍了其起源、内核实现、用户接口以及使用,有兴趣的读者可以读2.6.13的相关源码来进一步了解其实现细节。