监听风云 | Inotify 实现原理

[[401919]]

本文转载自微信公众号「Linux内核那些事」,作者songsong001。转载本文请联系Linux内核那些事公众号。

重要的数据结构

鲁迅先生说过:程序 = 数据结构 + 算法

想想如果让我们来设计 inotify 应该如何实现呢?下面来分析一下:

  • 我们知道,inotify 是用来监控文件或目录的变动事件,所以应该定义一个对象来存储被监听的文件或目录列表和它们所发生的事件列表(在内核中定义了 inotify_device 对象来存储被监听的文件列表和事件列表)。
  • 另外,当对被监听的文件或目录进行读写操作时会触发相应的事件产生。所以,应该在读写操作相关的系统调用中嵌入产生事件的动作(在内核中由 inotify_dev_queue_event 函数产生事件)。

在介绍 inotify 的实现前,我们先来了解下其原理。inotify 的原理如下:

当用户调用 read 或者 write 等系统调用对文件进行读写操作时,内核会把事件保存到 inotify_device 对象的事件队列中,然后唤醒等待 inotify 事件的进程。正所谓一图胜千言,所以我们通过下图来描述此过程:

从上图可知,当应用程序调用 read 函数读取文件的内容时,最终会调用 inotify_dev_queue_event 函数来触发事件,调用栈如下:

  1. read() 
  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 监听的对象和发生的事件,其定义如下:

  1. 1struct inotify_device { 
  2. 2    wait_queue_head_t       wq; 
  3. 3    ... 
  4. 4    struct list_head        events; 
  5. 5    ... 
  6. 6    struct inotify_handle   *ih; 
  7. 7    unsigned int            event_count; 
  8. 8    unsigned int            max_events; 
  9. 9}; 

下面我们介绍一下各个字段的作用:

  • wq:正在等待当前 inotify 发生事件的进程列表。
  • events:保存由 inotify 监听的文件或目录所发生的事件。
  • ih:内核用来存储 inotify 监听的文件或目录,下面会介绍。
  • event_count:inotify 监听的文件或目录所发生的事件数量。
  • max_events:inotify 能够保存最大的事件数量。

下图描述了 inotify_device 对象中两个比较重要的队列(等待队列 和 事件队列):

当事件队列中有数据时,就可以通过调用 read 函数来读取这些事件。

2. inotify_kernel_event对象

内核使用 inotify_kernel_event 对象来存储一个事件,其定义如下:

  1. struct 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 对象的定义:

  1. struct 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 对象来表示一个被监听的对象。其定义如下:

  1. struct 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 的实现:

  1. long sys_inotify_init(void) 
  2.     struct inotify_device *dev; 
  3.     struct inotify_handle *ih; 
  4.     struct user_struct *user
  5.     struct file *filp; 
  6.     int fd, ret; 
  7.  
  8.     // 1. 获取一个没用被占用的文件描述符 
  9.     fd = get_unused_fd(); 
  10.     ... 
  11.     // 2. 获取一个文件对象 
  12.     filp = get_empty_filp(); 
  13.     ... 
  14.     // 3. 创建一个 inotify_device 对象 
  15.     dev = kmalloc(sizeof(struct inotify_device), GFP_KERNEL); 
  16.     ... 
  17.     // 4. 创建一个 inotify_handle 对象 
  18.     ih = inotify_init(&inotify_user_ops); 
  19.     ... 
  20.     // 5. 把 inotify_handle 对象与 inotify_device 对象进行绑定 
  21.     dev->ih = ih; 
  22.     // 6. 设置文件对象的操作函数列表为:inotify_fops 
  23.     filp->f_op = &inotify_fops; 
  24.     ... 
  25.     // 7. 将 inotify_device 对象绑定到文件对象的 private_data 字段中 
  26.     filp->private_data = dev; 
  27.     ... 
  28.     // 8. 把文件句柄与文件对象进行映射 
  29.     fd_install(fd, filp); 
  30.  
  31.     return fd; 

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 等接口的实现,其定义如下:

  1. static 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 函数的实现如下:

 

  1. long sys_inotify_add_watch(int fd, const char __user *path, u32 mask) 
  2.     struct inode *inode; 
  3.     struct inotify_device *dev; 
  4.     struct nameidata nd; 
  5.     struct file *filp; 
  6.     int ret, fput_needed; 
  7.     unsigned flags = 0; 
  8.  
  9.     // 通过文件句柄获取文件对象 
  10.     filp = fget_light(fd, &fput_needed); 
  11.     ... 
  12.     // 获取文件或目录对应的 inode 对象 
  13.     ret = find_inode(path, &nd, flags); 
  14.     ... 
  15.     inode = nd.dentry->d_inode; 
  16.     // 从文件对象的 private_data 字段获取对应的 inotify_device 对象 
  17.     dev = filp->private_data; 
  18.     ... 
  19.     // 创建一个新的 inotify_watch 对象 
  20.     if (ret == -ENOENT) 
  21.         ret = create_watch(dev, inode, mask); 
  22.     ... 
  23.     return ret; 

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 系统调用的调用栈:

  1. read() 
  2. └→ sys_read() 
  3.    └→ vfs_read() 
  4.       └→ fsnotify_access() 
  5.          └→ inotify_inode_queue_event() 
  6.             └→ inotify_dev_queue_event() 

下面我们来分析一下 inotify_dev_queue_event 函数的实现:

  1. static void 
  2. inotify_dev_queue_event(struct inotify_watch *w, u32 wd,   
  3.     u32 mask, u32 cookie, const char *name, struct inode *ignored) 
  4.     struct inotify_user_watch *watch; 
  5.     struct inotify_device *dev; 
  6.     struct inotify_kernel_event *kevent, *last
  7.  
  8.     watch = container_of(w, struct inotify_user_watch, wdata); 
  9.    dev = watch->dev; 
  10.    ... 
  11.    // 1. 申请一个 inotify_kernel_event 事件对象 
  12.    if (unlikely(dev->event_count == dev->max_events)) 
  13.        kevent = kernel_event(-1, IN_Q_OVERFLOW, cookie, NULL); 
  14.    else 
  15.        kevent = kernel_event(wd, mask, cookie, name); 
  16.    ... 
  17.    // 2. 增加 inotify 事件队列的计数器 
  18.    dev->event_count++; 
  19.    // 3. 增加 inotify 事件队列所占用的内存大小 
  20.    dev->queue_size += sizeof(struct inotify_event) + kevent->event.len; 
  21.  
  22.    // 4. 把事件对象添加到 inotify 的事件队列中 
  23.    list_add_tail(&kevent->list, &dev->events); 
  24.  
  25.    // 5. 唤醒正在等待读取事件的进程 
  26.    wake_up_interruptible(&dev->wq); 
  27.    ... 

我们先来介绍一下 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 函数的实现:

  1. static inline void fsnotify_access(struct dentry *dentry) 
  2.     struct inode *inode = dentry->d_inode; 
  3.     u32 mask = IN_ACCESS; // 指定事件类型为 IN_ACCESS 
  4.  
  5.     if (S_ISDIR(inode->i_mode)) 
  6.         mask |= IN_ISDIR; // 如果是目录, 增加 IN_ISDIR 标志 
  7.     ... 
  8.     // 创建事件 
  9.     inotify_inode_queue_event(inode, mask, 0, NULLNULL);  

从上面的分析可知,当发生读事件时,由 fsnotify_access 函数指定事件类型为 IN_ACCESS。在 include/linux/fsnotify.h 文件中还实现了其他事件的触发函数,有兴趣的可以自行查阅此文件 。

总结

inotify 的实现过程总结为以下两点:

当用户调用读写、创建或删除文件的系统调用时,内核会注入相应的事件触发函数来产生一个事件,并且添加到 inotify 的事件队列中。

唤醒等待读取事件的进程,当进程被唤醒后,就可以通过调用 read 函数来读取 inotify 事件队列中的事件。

 

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/133355.html<

(0)
运维的头像运维
上一篇2025-02-27 10:59
下一篇 2025-02-27 11:00

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注