监听风云 | inotify 实现原理
在《监听风云 - inotify 介绍》一文中,我们介绍了 inotify
的使用。为了能更深入理解 inotify
的原理,本文开始介绍 inotify
功能的实现过程。
重要的数据结构
鲁迅先生说过:程序 = 数据结构 + 算法
想想如果让我们来设计 inotify
应该如何实现呢?下面来分析一下:
我们知道,
inotify
是用来监控文件或目录的变动事件,所以应该定义一个对象来存储被监听的文件或目录列表和它们所发生的事件列表(在内核中定义了inotify_device
对象来存储被监听的文件列表和事件列表)。
另外,当对被监听的文件或目录进行读写操作时会触发相应的事件产生。所以,应该在读写操作相关的系统调用中嵌入产生事件的动作(在内核中由
inotify_dev_queue_event
函数产生事件)。
在介绍 inotify
的实现前,我们先来了解下其原理。inotify
的原理如下:
当用户调用
read
或者write
等系统调用对文件进行读写操作时,内核会把事件保存到inotify_device
对象的事件队列中,然后唤醒等待inotify
事件的进程。正所谓一图胜千言,所以我们通过下图来描述此过程:
从上图可知,当应用程序调用 read
函数读取文件的内容时,最终会调用 inotify_dev_queue_event
函数来触发事件,调用栈如下:
1read()
2└→ sys_read()
3 └→ vfs_read()
4 └→ fsnotify_access()
5 └→ inotify_inode_queue_event()
6 └→ inotify_dev_queue_event()
inotify_dev_queue_event
函数主要完成两个工作:
创建一个表示事件的
inotify_kernel_event
对象,并且把其插入到inotify_device
对象的events
列表中。唤醒正在等待
inotify
发生事件的进程,等待的进程放置在inotify_device
对象的wq
字段中。
上面主要涉及到两个对象,inotify_device
和 inotify_kernel_event
,我们先来介绍一下这两个对象的作用。
inotify_device
:内核使用此对象来描述一个inotify
,是inotify
的核心对象。intoify_kernel_event
:内核使用此对象来描述一个事件。
我们来看看这两个对象的定义。
1. inotify_device对象
内核使用 inotify_device
来管理 inotify
监听的对象和发生的事件,其定义如下:
1struct inotify_device {
2 wait_queue_head_t wq;
3 ...
4 struct list_head events;
5 ...
6 struct inotify_handle *ih;
7 unsigned int event_count;
8 unsigned int max_events;
9};
下面我们介绍一下各个字段的作用:
wq
:正在等待当前inotify
发生事件的进程列表。events
:保存由inotify
监听的文件或目录所发生的事件。ih
:内核用来存储inotify
监听的文件或目录,下面会介绍。event_count
:inotify
监听的文件或目录所发生的事件数量。max_events
:inotify
能够保存最大的事件数量。
下图描述了 inotify_device
对象中两个比较重要的队列(等待队列
和 事件队列
):
当事件队列中有数据时,就可以通过调用 read
函数来读取这些事件。
2. inotify_kernel_event对象
内核使用 inotify_kernel_event
对象来存储一个事件,其定义如下:
1struct inotify_kernel_event {
2 struct inotify_event event;
3 struct list_head list;
4 char *name;
5};
可以看出,inotify_kernel_event
对象只是对 inotify_event
对象进行扩展而已,而我们在《监听风云 - inotify介绍》一文中已经介绍过 inotify_event
对象。
inotify_kernel_event
对象在 inotify_event
对象的基础上增加了 list
字段和 name
字段:
list
:用于把所有由inotify
监听的文件或目录所发生的事件连接起来,name
:用于记录发生事件的文件名或目录名。
3. inotify_handle对象
在 inotify_device
对象中,有个类型为 inotify_handle
的字段 ih
,这个字段主要用来存储 inotify
监听的文件或目录。我们来看看 inotify_handle
对象的定义:
1struct inotify_handle {
2 struct idr idr;
3 ...
4 struct list_head watches;
5 ...
6 const struct inotify_operations *in_ops;
7};
下面来介绍一下 inotify_handle
对象的各个字段作用:
idr
:ID生成器,用于生成被监听对象(文件或目录)的ID。watches
:inotify
监听的对象(文件或目录)列表。in_ops
:当事件发生时,被inotify
回调的函数列表。
4. inotify_watch对象
内核使用 inotify_handle
来存储被监听的对象列表,那么被监听对象是个什么东西呢?内核中使用 inotify_watch
对象来表示一个被监听的对象。其定义如下:
1struct inotify_watch {
2 struct list_head h_list;
3 struct list_head i_list;
4 ...
5 struct inotify_handle *ih;
6 struct inode *inode;
7 __s32 wd;
8 __u32 mask;
9};
下面介绍一下 inotify_watch
对象各个字段的作用:
h_list
:用于把属于同一个inotify
监听的对象连接起来。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
的实现:
1long sys_inotify_init(void)
2{
3 struct inotify_device *dev;
4 struct inotify_handle *ih;
5 struct user_struct *user;
6 struct file *filp;
7 int fd, ret;
8
9 // 1. 获取一个没用被占用的文件描述符
10 fd = get_unused_fd();
11 ...
12 // 2. 获取一个文件对象
13 filp = get_empty_filp();
14 ...
15 // 3. 创建一个 inotify_device 对象
16 dev = kmalloc(sizeof(struct inotify_device), GFP_KERNEL);
17 ...
18 // 4. 创建一个 inotify_handle 对象
19 ih = inotify_init(&inotify_user_ops);
20 ...
21 // 5. 把 inotify_handle 对象与 inotify_device 对象进行绑定
22 dev->ih = ih;
23 // 6. 设置文件对象的操作函数列表为:inotify_fops
24 filp->f_op = &inotify_fops;
25 ...
26 // 7. 将 inotify_device 对象绑定到文件对象的 private_data 字段中
27 filp->private_data = dev;
28 ...
29 // 8. 把文件句柄与文件对象进行映射
30 fd_install(fd, filp);
31
32 return fd;
33}
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
等接口的实现,其定义如下:
1static const struct file_operations inotify_fops = {
2 .poll = inotify_poll,
3 .read = inotify_read,
4 .release = inotify_release,
5 ...
6};
所以,当调用 read
函数读取 inotify
的句柄时,就会触发调用 inotify_read
函数读取 inotify
事件队列中的事件。
2. inotify_add_watch 函数
当调用 inotify_init
函数创建好 inotify
句柄后,就可以通过调用 inotify_add_watch
函数向 inotify
句柄添加要监控的文件或目录。inotify_add_watch
函数的实现如下:
1long sys_inotify_add_watch(int fd, const char __user *path, u32 mask)
2{
3 struct inode *inode;
4 struct inotify_device *dev;
5 struct nameidata nd;
6 struct file *filp;
7 int ret, fput_needed;
8 unsigned flags = 0;
9
10 // 通过文件句柄获取文件对象
11 filp = fget_light(fd, &fput_needed);
12 ...
13 // 获取文件或目录对应的 inode 对象
14 ret = find_inode(path, &nd, flags);
15 ...
16 inode = nd.dentry->d_inode;
17 // 从文件对象的 private_data 字段获取对应的 inotify_device 对象
18 dev = filp->private_data;
19 ...
20 // 创建一个新的 inotify_watch 对象
21 if (ret == -ENOENT)
22 ret = create_watch(dev, inode, mask);
23 ...
24 return ret;
25}
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
系统调用的调用栈:
1read()
2└→ sys_read()
3 └→ vfs_read()
4 └→ fsnotify_access()
5 └→ inotify_inode_queue_event()
6 └→ inotify_dev_queue_event()
下面我们来分析一下 inotify_dev_queue_event
函数的实现:
1static void
2inotify_dev_queue_event(struct inotify_watch *w, u32 wd,
3 u32 mask, u32 cookie, const char *name, struct inode *ignored)
4{
5 struct inotify_user_watch *watch;
6 struct inotify_device *dev;
7 struct inotify_kernel_event *kevent, *last;
8
9 watch = container_of(w, struct inotify_user_watch, wdata);
10 dev = watch->dev;
11 ...
12 // 1. 申请一个 inotify_kernel_event 事件对象
13 if (unlikely(dev->event_count == dev->max_events))
14 kevent = kernel_event(-1, IN_Q_OVERFLOW, cookie, NULL);
15 else
16 kevent = kernel_event(wd, mask, cookie, name);
17 ...
18 // 2. 增加 inotify 事件队列的计数器
19 dev->event_count++;
20 // 3. 增加 inotify 事件队列所占用的内存大小
21 dev->queue_size += sizeof(struct inotify_event) + kevent->event.len;
22
23 // 4. 把事件对象添加到 inotify 的事件队列中
24 list_add_tail(&kevent->list, &dev->events);
25
26 // 5. 唤醒正在等待读取事件的进程
27 wake_up_interruptible(&dev->wq);
28 ...
29}
我们先来介绍一下 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
函数的实现:
1static inline void fsnotify_access(struct dentry *dentry)
2{
3 struct inode *inode = dentry->d_inode;
4 u32 mask = IN_ACCESS; // 指定事件类型为 IN_ACCESS
5
6 if (S_ISDIR(inode->i_mode))
7 mask |= IN_ISDIR; // 如果是目录, 增加 IN_ISDIR 标志
8 ...
9 // 创建事件
10 inotify_inode_queue_event(inode, mask, 0, NULL, NULL);
11}
从上面的分析可知,当发生读事件时,由 fsnotify_access
函数指定事件类型为 IN_ACCESS
。在 include/linux/fsnotify.h 文件中还实现了其他事件的触发函数,有兴趣的可以自行查阅此文件 。
总结
inotify
的实现过程总结为以下两点:
当用户调用读写、创建或删除文件的系统调用时,内核会注入相应的事件触发函数来产生一个事件,并且添加到
inotify
的事件队列中。唤醒等待读取事件的进程,当进程被唤醒后,就可以通过调用
read
函数来读取inotify
事件队列中的事件。