本文转载自微信公众号《Linux内核那些事儿》,作者songsong001。转载本文请联系Linux内核那些事儿公众号。重要的数据结构鲁迅先生说过:程序=数据结构+算法想想如果我们设计了inotify怎么实现?下面分析一下:我们知道inotify是用来监听文件或目录变化事件的,所以它应该定义一个对象来存放被监听的文件或目录列表及其事件列表(内核中定义了inotify_device对象来存放被监听的文件或目录列表)文件列表和事件列表)。另外,当被监控的文件或目录被读取或写入时,会触发相应的事件。因此,产生事件的动作应该嵌入到读写操作相关的系统调用中(事件由内核中的inotify_dev_queue_event函数产生)。在介绍inotify的实现之前,我们先了解一下它的原理。inotify的原理是这样的:当用户调用read或write等系统调用读写文件时,内核会将事件保存到inotify_device对象的事件队列中,然后唤醒等待inotify的进程事件。俗话说,一图胜千言,所以我们通过下图来描述这个过程:从上图可以看出,当应用程序调用read函数读取文件内容时,最终会调用inotify_dev_queue_event函数触发事件。调用栈如下:read()└→sys_read()└→vfs_read()└→fsnotify_access()└→inotify_inode_queue_event()└→inotify_dev_queue_event()inotify_dev_queue_event函数主要完成两个任务:创建一个代表事件的inotify_kernel_event对象并将其插入到inotify_device对象事件列表中。唤醒正在等待inotify事件发生的进程,等待进程放在inotify_device对象的wq字段中。上面主要涉及到inotify_device和inotify_kernel_event两个对象,我们先介绍一下这两个对象的作用。inotify_device:内核用这个对象来描述一个inotify,它是inotify的核心对象。intoify_kernel_event:内核使用这个对象来描述一个事件。我们来看看这两个对象的定义。1、inotify_device对象内核使用inotify_device来管理inotify监听的对象和发生的事件。定义如下:下面是各个字段的作用:wq:等待当前inotify事件发生的进程列表。events:保存在inotify侦听的文件或目录上发生的事件。ih:内核用于存放inotify监控的文件或目录,下面会介绍。event_count:inotify监控的文件或目录中发生的事件数。max_events:inotify可以保存的最大事件数。下图描述了inotify_device对象中的两个重要队列(等待队列和事件队列):当事件队列中有数据时,可以调用read函数读取这些事件。2.inotify_kernel_event对象内核使用inotify_kernel_event对象来存储一个事件,其定义如下:structinotify_kernel_event{structinotify_eventevent;structlist_headlist;char*name;};可见inotify_kernel_event对象只是inotify_event对象的扩展,而我们在一文中已经介绍了inotify_event对象。inotify_kernel_event对象在inotify_event对象的基础上增加了list字段和name字段:list:用于连接inotify监控的文件或目录中发生的所有事件,name:用于记录事件的文件名或目录名.3、inotify_handle对象在inotify_device对象中,有一个inotify_handle类型的字段ih,主要用来存放inotify监控的文件或目录。我们看一下inotify_handle对象的定义:structinotify_handle{structidridr;...structlist_headwatches;...conststructinotify_operations*in_ops;};下面介绍一下inotify_handle对象的各个字段的作用:idr:ID生成器,用于生成被监控对象(文件或目录)的ID。watches:inotify监控的对象(文件或目录)列表。in_ops:事件发生时inotify回调的函数列表。4、inotify_watch对象内核使用inotify_handle来存储被监控对象的列表,那么被监控的对象是什么?inotify_watch对象在内核中用来表示一个被监控的对象。其定义如下:下面介绍inotify_watch对象的各个字段的作用:h_list:用于将属于同一个inotifymonitor的对象连接起来。i_list:由于同一个文件或目录可以被多个inotify监控,所以使用这个字段连接所有监控同一个文件的inotify_handle对象。ih:指向它所属的inotify_handle对象。inode:由于在Linux内核中,每个文件或目录都由一个inode对象来描述,该字段就是指向被监控文件或目录的inode对象。wd:被监控对象的ID(或描述符)。mask:被监控的事件类型(在文章《监听风云 - inotify介绍》中有介绍)。现在,我们通过下图来描述inotify_device、inotify_handle和inotify_watch之间的关系:inotify函数实现上面我们已经介绍了inotify函数涉及的所有数据结构。有了上面的基础,现在我们就可以开始分析inotify功能的实现了。1.inotify_init函数在《监听风云 - inotify介绍》一文中有介绍。要使用inotify函数,首先要调用inotify_init函数创建一个inotify句柄,inotify_init函数最终会调用内核函数sys_inotify_init。下面分析一下sys_inotify_init的实现:longsys_inotify_init(void){structinotify_device*dev;structinotify_handle*ih;structuser_struct*user;structfile*filp;intfd,ret;//1.获取一个无用且被占用的文件描述符fd=get_unused_fd();...//2.获取文件对象filp=get_empty_filp();...//3.创建一个inotify_device对象dev=kmalloc(sizeof(structinotify_device),GFP_KERNEL);...//4.创建一个inotify_handle对象ih=inotify_init(&inotify_user_ops);...//5.绑定inotify_handle对象和inotify_device对象dev->ih=ih;//6.设置文件对象的操作函数列表为:inotify_fopsfilp->f_op=&inotify_fops;...//7.将inotify_device对象绑定到文件对象的private_data字段filp->private_data=dev;...//8.将文件句柄映射到文件对象fd_install(fd,filp);returnfd;}sys_inotify_init函数主要完成以下任务:调用get_unused_fd函数从进程中获取一个未使用的文件描述符(句柄)。调用get_empty_filp获取一个文件对象。调用kmalloc函数申请一个inotify_device对象。调用inotify_init函数创建并初始化一个inotify_handle对象。将inotify_handle对象绑定到inotify_device对象。设置文件对象的操作函数列表为:inotify_fops,主要提供read、poll等接口的实现。将inotify_device对象绑定到文件对象的private_data字段中。将文件描述符映射到文件对象。将文件描述符返回给应用层。从上面的实现可以看出,sys_inotify_init函数主要是创建inotify_device对象和inotify_handle对象,并将它们与文件对象相关联。另外需要注意的是,在sys_inotify_init函数中,还设置了文件对象的操作函数集为inotify_fops,主要提供了read、poll等接口的实现,其定义如下:staticconststructfile_operationsinotify_fops={.poll=inotify_poll,.read=inotify_read,.release=inotify_release,...};因此,在调用read函数读取inotify句柄时,会触发调用inotify_read函数读取inotify事件队列中的事件。2.inotify_add_watch函数调用inotify_init函数创建inotify句柄后,可以通过调用inotify_add_watch函数将需要监控的文件或目录添加到inotify句柄中。inotify_add_watch函数的实现如下:longsys_inotify_add_watch(intfd,constchar__user*path,u32mask){structinode*inode;structinotify_device*dev;structnameidatand;structfile*filp;intret,fput_needed;unsignedflags=0;//获取文件对象filp=fget_light通过文件句柄(fd,&fput_needed);...//获取文件或目录对应的inode对象ret=find_inode(path,&nd,flags);...inode=nd.dentry->d_inode;//从文件对象的private_data字段获取对应的inotify_device对象dev=filp->private_data;...//新建一个inotify_watch对象if(ret==-ENOENT)ret=create_watch(dev,inode,mask);...returnret;}sys_inotify_add_watch函数主要完成以下任务:调用fget_light函数获取inotify句柄对应的文件对象。调用find_inode函数获取path路径对应的inode对象,即获取待监控文件或目录对应的inode对象。从inotify文件对象的private_data字段中获取对应的inotify_device对象。调用create_watch函数新建一个inotify_watch对象,并将这个inotify_watch对象添加到inotify_handle对象的watches列表和inode对象的inotify_watches列表中。inotify最关键的部分是事件通知,这就是inotify事件的产生方式。正如本文第一部分介绍的,当用户调用read系统调用读取文件内容时,最终会调用inotify_dev_queue_event函数产生一个事件。我们先回顾一下read系统调用的调用栈:read()└→sys_read()└→vfs_read()└→fsnotify_access()└→inotify_inode_queue_event()└→inotify_dev_queue_event()下面分析inotify_dev_queue_event函数的实现:staticvoidinotify_dev_queue_event(structinotify_watch*w,u32wd,u32mask,u32cookie,constchar*name){structinotify_user_watch*watch;structinotify_device*dev;structinotify_kernel_event*kevent,*last;watch=container_of(w,structinotify_user_watch,wdata);dev=watch->dev;...//1。申请一个inotify_kernel_event事件对象if(unlikely(dev->event_count==dev->max_events))kevent=kernel_event(-1,IN_Q_OVERFLOW,cookie,NULL);elsekevent=kernel_event(wd,mask,cookie,name);。..//2。添加inotify事件队列计数器dev->event_count++;//3.增加inotify事件队列占用的内存大小dev->queue_size+=sizeof(structinotify_event)+kevent->event.len;//4.将事件对象添加到队列中的inotify事件list_add_tail(&kevent->list,&dev->events);//5.唤醒正在等待读取事件的进程wake_up_interruptible(&dev->wq);...先介绍下inotify_dev_queue_event函数各个参数的含义:w:被监控对象,用于描述被监控的文件或目录wd:监听的文件或目录监听对象的ID。mask:发生的事件类型,可以参考文章《监听风云 - inotify介绍》。cookie:较少使用,忽略。name:事件发生的文件或目录的名称。ignored:事件发生的文件或目录的inode对象,本函数中未使用。inotify_dev_queue_event函数主要完成以下任务:通过调用kernel_event函数申请一个inotify_kernel_event事件对象。增加inotify事件队列的计数器。增加inotify事件队列使用的内存大小。将第一步创建的事件对象添加到inotify的事件队列中。唤醒等待读取事件的进程(因为事件已经发生)。从上面的分析可以看出,inotify_dev_queue_event函数只负责创建一个事件对象,并将其加入到inotify的事件队列中。但是哪一步指定了什么事件呢?我们可以分析read系统调用的调用栈,发现在fsnotify_access函数中指定了事件的类型。我们看一下fsnotify_access函数的实现:staticinlinevoidfsnotify_access(structdentry*dentry){structinode*inode=dentry->d_inode;u32mask=IN_ACCESS;//指定事件类型为IN_ACCESSif(S_ISDIR(inode->i_mode))mask|=IN_ISDIR;//如果是目录,加上IN_ISDIR标志...//创建一个事件inotify_inode_queue_event(inode,mask,0,NULL,NULL);}从上面的分析可以看出,当一个read事件发生时,fsnotify_access函数指定的事件类型为IN_ACCESS。其他事件的触发函数也在include/linux/fsnotify.h文件中实现。有兴趣的可以自行参考这个文件。小结inotify的实现过程总结为以下两点:当用户调用系统调用进行文件的读、写、创建或删除操作时,内核会注入相应的事件触发函数产生一个事件,并添加到inotify的事件队列。唤醒等待读取事件的进程。当进程被唤醒后,可以调用read函数读取inotify事件队列中的事件。
