浅析 Linux inotify机制

简介

在我们工作中,经常会遇到一些场景: 在系统状况发生变化时, 我们能够及时地被告知.而不是等到我们想起来,才去查看是否变化.特别是一些重要的事情, 如系统故障, 磁盘空间等等.这些硬件级别的"异步通知", 监控系统已经为我们实现了. 那么文件系统级别的呢? 或者举个更加具体的例子: 想要实时同步新文件, 做镜像备份, 那么怎样才能实现实时? 计划任务? 这固然可以, 但是在计划任务调度之间, 总会存在这么小间隔的"非实时", 其实大神们早已为我们想出更好的解决方案: inotify机制

顾名思义, inotify 是一种文件系统的变化通知机制,如文件增加、删除等事件可以立刻让用户态得知

事实上, 在inotify之前已经存在一种类似的机制: dnotify, 但据文档介绍, 它存在很多缺陷:

  • 对于想监视的每一个目录,用户都需要打开一个文件描述符,因此如果需要监视的目录较多,将导致打开许多文件描述符,特别是,如果被监视目录在移动介质上(如光盘和 USB 盘),将导致无法 umount 这些文件系统,因为使用 dnotify 的应用打开的文件描述符在使用该文件系统。
  • dnotify 是基于目录的,它只能得到目录变化事件,当然在目录内的文件的变化会影响到其所在目录从而引发目录变化事件,但是要想通过目录事件来得知哪个文件变化,需要缓存许多 stat 结构的数据。
  • Dnotify 的接口非常不友好,它使用 signal。

 而作为替代品的inotify却有更为突出的表现,它克服了dnotify的缺点之外,还有更多的优点:

  • inotify 不需要对被监视的目标打开文件描述符,而且如果被监视目标在可移动介质上,那么在 umount 该介质上的文件系统后,被监视目标对应的 watch 将被自动删除,并且会产生一个 umount 事件。
  • inotify 既可以监视文件,也可以监视目录。
  • inotify 使用系统调用而非 SIGIO 来通知文件系统事件。
  • inotify 使用文件描述符作为接口,因而可以使用通常的文件 I/O 操作select 和 poll 来监视文件系统的变化。

 它可以监控的文件系统事件包括:

事件掩码文件系统事件
IN_ACCESS文件被访问
IN_MODIFY文件内容被修改
IN_ATTRIB文件属性被修改( 如 chmod )
IN_CLOSE_WRITE可写文件被关闭
IN_CLOSE_NOWRITE不可写文件被关闭
IN_MOVED_FROM文件被移走( 如 mv )
IN_MOVED_TO文件被移来( 如 mv, cp )
IN_OPEN文件被打开
IN_CREATE文件被创建
IN_DELETE文件被删除
IN_DELETE_SELF自删除( 一个可执行文件在执行时删除自己)
IN_MOVE_SELF自移动( 一个可执行文件在执行时移动自己 )
IN_UNMOUNT宿主文件系统被卸载
IN_CLOSE文件被关闭等同于(IN_CLOSE_WRITE | IN_CLOSE_NOWRITE)
IN_MOVE文件被移动 等同于 (IN_MOVED_FROM | IN_MOVED_TO)

如何使用

在用户态, inotify通过三个系统调用和返回的文件描述符上的文件I/O操作来使用, 那么第一步就是创建inotify实例:

int fd = inotify_init();

每个inotify实例对应着一个独立的排序的队列.

文件系统的变化事件, 被称作watches的一个对象管理, 每一个watch是一个二元组(目标, 事件掩码), 目标可以是文件或者目录, 而事件掩码则是上述表格的内容.每一个对应inotify事件.watch对象通过watch描述符的引用,watches通过文件或者目录的路径名来添加.如果是目录, watch将返回该目录下所有关注的发生的事件.

添加一个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结构表示:

取自sys/inotify.h

/* Structure describing an inotify event.  */
struct inotify_event
{
  int wd;       /* Watch descriptor.  */
  uint32_t mask;    /* Watch mask.  */
  uint32_t cookie;  /* Cookie to synchronize two events.  */
  uint32_t len;     /* Length (including NULs) of name.  */
  char name __flexarr;  /* Name.  */
};

结构体中的wd为被监视目标的watch描述符, mask为事件掩码, name为路径名, len为name字符的长度

通过read调用来获取事件:

size_t len = read(fd, buff, BUF_LEN);

buff 是一个inotify_event结构的结构体指针, BUF_LEN是指定要读取的总长度, buff长度要大于等于BUF_LEN. read调用返回的时间数取决于BUF_LEN以及时间中文件名的长度, len为实际读取的字节数, 即获取的事件中总长度.

可以在函数 inotify_init() 返回的文件描述符 fd 上使用 select() 或poll(), 也可以在 fd 上使用 ioctl 命令 FIONREAD 来得到当前队列的长度。close(fd)将删除所有添加到 fd 中的 watch 并做必要的清理。

简单实现

#include <stdio.h>
#include <sys/inotify.h>
#include <string.h>
#define MAX_BUFFER_SIZE 4096
int main()
{
    int fd, wd;
    int len, index;
    char buffer[2048];
    struct inotify_event * event;
    char *path = "/tmp/test";

    if((fd = inotify_init()) <0)
    {
        printf("Failed to initialize inotify\n");
        return 1;
    }
    // 通过inotify_add_watch向初始化的fd添加事件
    if((wd = inotify_add_watch(fd, path, IN_CLOSE_WRITE | IN_CREATE)) < 0)
    {
        printf("Can't add watch for %s\n", path);
        return 1;
    }
    while(len = read(fd, buffer, MAX_BUFFER_SIZE))
    {
        index = 0;
        while(index < len)
        {
            // 强制将字符数组转换成 inotify_event 结构体
            event = (struct inotify_event *)(buffer + index);
            if (event->wd != wd)
                continue;
            if(event->mask & IN_CLOSE_WRITE)
                printf("file %s is closed for writing.\n", event->name);
            else if(event->mask & IN_CREATE)
                printf("file %s is created.\n", event->name);
            // 移动到下一个事件
            index += sizeof(struct inotify_event) + event->len;
        }
    }
    return 0;


}
[root@iZ23pynfq19Z ~]# mkdir /tmp/test
[root@iZ23pynfq19Z ~]# gcc 1.c && ./a.out
...处于堵塞状态

## 当另一个终端创建了 文件11 之后的输出
file 11 is created.
file 11 is closed for writing.


----------------------- 另一个终端 ---------------------
[root@iZ23pynfq19Z ~]# cd /tmp/test/
[root@iZ23pynfq19Z test]# touch 11

例子比较简单, 请大家见谅~ 后期咱们将其完善得更好吧~

----------------------------------- 可爱的分割线 -------------------------------

咱们也不能光会用, 了解下本质原理也是很不错的 ( 谷歌的结果 )

内核实现机理

在内核中, 每一个inotify实例对应这一个inotify_device结构

struct inotify_device {
        wait_queue_head_t       wq;             /* wait queue for i/o */
        struct idr              idr;            /* idr mapping wd -> watch */
        struct semaphore        sem;            /* protects this bad boy */
        struct list_head        events;         /* list of queued events */
        struct list_head        watches;        /* list of watches */
        atomic_t                count;          /* reference count */
        struct user_struct      *user;          /* user who opened this dev */
        unsigned int            queue_size;     /* size of the queue (bytes) */
        unsigned int            event_count;    /* number of pending events */
        unsigned int            max_events;     /* maximum number of events */
        u32                     last_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_head        d_list; /* entry in inotify_device's list */
        struct list_head        i_list; /* entry in inode's list */
        atomic_t                count;  /* reference count */
        struct inotify_device   *dev;   /* associated device */
        struct inode            *inode; /* associated inode */
        s32                     wd;     /* watch descriptor */
        u32                     mask;   /* 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() 时被释放.

无论是目录还是文件, 在内核中都对应这一个inode结构, inotify 系统在inode结构中增加了两个字段:

#ifdef CONFIG_INOTIFY
	struct list_head	inotify_watches; /* watches on this inode */
	struct semaphore	inotify_sem;	/* protects the watches list */
#endif

inotify_watches 是在被监视目标上的watch列表, 每当用户调用 inotify_add_watch() 时, 内核就为添加的 watch 创建一个inotify_watch 结构, 并把它插入到被监视目标对应的inotify 的 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上.

欢迎各位大神指点 转载请注明来源:https://my.oschina.net/u/2291453/blog/833919

转载于:https://my.oschina.net/LinBigR/blog/833919

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值