使用 Linux inotify 进行文件监控。

参考:inotify(7) — Linux manual page

目录


文章目录

  • 目录
  • 1. inotify 简介
  • inotify 原理
  • inotify API
  • inotify_init
  • inotify_add_watch
  • inotify_rm_watch
  • inotify 数据结构
  • 2. inotify 使用
  • 示例1:inotify + read
  • 示例2:inotify + epoll


1. inotify 简介

Linux 提供的 inotify 是一种异步文件监控机制,可以用来监控文件系统的事件,包括访问、读写、权限、删除、移动等事件。

inotify 既可以监控单个文件,也可以监控整个目录,当监控的对象是一个目录的时候,目录本身和目录下的文件/文件夹都是被监控的对象。但是不能递归监控子目录,如果想要监控子目录下的文件,需要自己通过递归的方法将所有子目录都添加到监控中。

此种机制出现的目的是当内核空间发生某种事件之后,可以立即通知到用户空间,方便用户对此做出相应的操作。

inotify 原理

inotify 是一个内核用于通知用户空间程序文件系统变化的机制。

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

  1. 当用户调用读写、创建或删除的系统调用时,内核会注入相应的事件触发函数来产生一个事件,并且添加到 inotify 的事件队列中。
  2. 唤醒等待读取事件的进程,当进程被唤醒后,用户就可以通过调用 read 函数来读取 inotify 事件队列中的事件,进而获知文件的变化。

此处只是简单记录了下 inotify 的原理,想了解详细过程的朋友还请参考如上的参考文献。

inotify API

inotify 三个常用的 API :

inotify_init()
inotify_add_watch()
inotify_rm_watch()

inotify_init

原型:

int fd = inotify_init(void);

用于创建一个inotify的实例,然后返回inotify事件队列的文件描述符。

inotify_add_watch

原型:

int wd = inotify_add_watch(int fd, const char* pathname, uint32_t mask);

该函数用于添加“watch list”,也就是监控列表。 可以是一个新的watch,也可以是一个已经存在的watch。其中 fd 就是 inotify_init() 的返回值,pathname 是要监控的目录或者文件的路径,mask 就是要监控的事件类型。见下文 inotify_event 中的 mask 字段。

该函数执行成功则返回一个unique的watch描述符。返回值是一个inotify标识,注意不要和 inotify_init() 的返回值搞混淆了。inotify_init() 的返回值是在读取事件、注册监听文件时使用,而 inotify_add_watch() 的返回值用来判断返回的事件属于哪个监听的文件(后面介绍inotify_event结构时会看到),以及移除监听文件时使用。

inotify_rm_watch

原型:

int inotify_rm_watch(int fd, int wd);

用于从 watch list 中移除监控的对象。其中 fdinotify_init() 的返回值,wdinotify_add_watch() 的返回值。

inotify API 常用使用流程:

inotify_init()
inotify_add_watch()
read() / epoll()
inotify_rm_watch()

inotify 数据结构

内核使用 inotify_event 表示一个文件事件。当有事件发生时(即监控的文件对象发生了变化),inotify 文件描述符会可读。此时使用 selectpollepoll 进行系统调用就会返回一个或者多个 inotify_event 文件事件对象。

inotify_event 数据结构:

struct inotify_event {
    int      wd;       /* 监控对象的watch描述符 */
    uint32_t mask;     /* 事件掩码 */
    uint32_t cookie;   /* 和rename事件相关 */
    uint32_t len;      /* name字段的长度 */
    char     name[];   /* 监控对象的文件或目录名 */
};

mask 所表示的事件监控类型如下:

事件

描述

IN_ACCESS

文件被访问

IN_ATTRIB

元数据被改变,例如权限、时间戳、扩展属性、链接数、UID、GID等

IN_CLOSE_WRITE

打开用于写的文件被关闭

IN_CLOSE_NOWRITE

不是打开用于写的文件被关闭

IN_CREATE

在监控的目录中创建了文件或目录

IN_DELETE

在监控的目录中删除了文件或目录

IN_DELETE_SELF

监控的文件或目录本身被删除

IN_MODIFY

文件被修改,这种事件会用到 inotify_event 中的 cookie

IN_MOVE_SELF

监控的文件或目录本身被移动

IN_MOVED_FROM

从监控的目录中移出文件

IN_MOVED_TO

向监控的目录中移入文件

IN_OPEN

文件或目录被打开

IN_ALL_EVENTS

包含了上面提到的所有事件

2. inotify 使用

示例1:inotify + read

示例:监听指定文件的创建和删除操作。

#include <sys/inotify.h>
#include <unistd.h>
#include <string>
#include <cstdio>

/*
struct inotify_event {
   int      wd;       // Watch descriptor
   uint32_t mask;     // Mask of events
   uint32_t cookie;   // Unique cookie associating related  events (for rename(2))
   uint32_t len;      // Size of name field
   char     name[];   // Optional null-terminated name
};
*/

int process_inotify_events(int fd) {
    char buf[512];
    struct inotify_event *event;
    int event_size = sizeof(struct inotify_event);

    // 检测事件是否发生,没有发生就会阻塞
    int read_len = read(fd, buf, sizeof(buf));

    // 如果read的返回值,小于inotify_event大小出现错误
    if (read_len < event_size) {
        printf("could not get event!\n");
        return -1;
    }

    // 因为read的返回值存在一个或者多个inotify_event对象,需要一个一个取出来处理
    int pos = 0;
    while (read_len >= event_size) {
        event = (struct inotify_event *) (buf + pos);
        if (event->len) {
            if (event->mask & IN_CREATE) {
                printf("create file: %s\n", event->name);
            } else {
                printf("delete file: %s\n", event->name);
            }
        }

        // 一个事件的真正大小:inotify_event 结构体所占用的空间 + inotify_event->name 所占用的空间
        int temp_size = event_size + event->len;
        read_len -= temp_size;
        pos += temp_size;
    }
    return 0;
}

int main(int argc, char **argv) {

    // inotify初始化
    int InotifyFd = inotify_init();
    if (InotifyFd == -1) {
        printf("inotify_init error!\n");
        return -1;
    }

    // 添加watch对象
    std::string filePath = "/home/test.log";
    int wd = inotify_add_watch(InotifyFd, filePath.c_str(), IN_CREATE | IN_DELETE);
    if (wd < 0) {
        printf("inotify_add_watch failed: %s\n", filePath.c_str());
        return -1;
    }

    // 处理事件
    int ret = process_inotify_events(InotifyFd);
    if (ret != 0) {
        return -1;
    }

    // 删除inotify的watch对象
    if (inotify_rm_watch(InotifyFd, wd) == -1) {
        printf("notify_rm_watch error!\n");
        return -1;
    }

    // 关闭inotify描述符
    close(InotifyFd);
    return 0;
}

示例2:inotify + epoll

示例:监听指定目录下的文件操作。

完整的代码见:inotify test

//
// Created by lixiaoqing on 2022/7/12.
//

#include <sys/inotify.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

#define EVENT_SIZE  ( sizeof (struct inotify_event) )
#define BUF_LEN     ( 1024 * ( EVENT_SIZE + 16 ) )

bool isQuited = false;

void quit() {
    isQuited = true;
}

int main(int argc, char *argv[]) {

    // inotify 初始化
    int fd = inotify_init();
    int wd = inotify_add_watch(fd, "/home/cc/tmp/", IN_MODIFY | IN_CREATE | IN_DELETE);  // 监听指定目录下的修改、创建、删除事件

    // 创建一个 epoll 句柄
    int epfd = epoll_create(256);
    struct epoll_event ev;
    ev.data.fd = fd;                // 设置要处理的事件相关的文件描述符
    ev.events = EPOLLIN | EPOLLET;  // 设置要处理的事件类型

    // 注册 epoll 事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

    // 循环监听事件
    char buffer[BUF_LEN];
    struct epoll_event events[20];  // 存储从内核得到的事件集合
    while (isQuited) {
        // 等待事件发生。返回需要处理的事件数目
        int nfds = epoll_wait(epfd, events, 20, 500);
        for (int i = 0; i < nfds; ++i) {
            /**
             * epoll_wait 会一直阻塞直到下面2种情况:
             *   1. 一个文件描述符触发了事件。
             *   2. 被一个信号处理函数打断,或者 timeout 超时。
             * 所以下面需要对 fd 进行过滤,判断是否是我们需要的 fd 产生了事件
             */
            if (events[i].data.fd != fd) {
                continue;
            }
            int length = read(fd, buffer, BUF_LEN);
            if (length < 0) {
                perror("read");
            }
            int pos = 0;
            while (pos < length) {
                struct inotify_event *event = (struct inotify_event *) &buffer[pos];
                if (event->len) {
                    if (event->mask & IN_CREATE) {
                        if (event->mask & IN_ISDIR) {
                            printf("The directory %s was created.\n", event->name);
                        } else {
                            printf("The file %s was created.\n", event->name);
                        }
                    } else if (event->mask & IN_DELETE) {
                        if (event->mask & IN_ISDIR) {
                            printf("The directory %s was deleted.\n", event->name);
                        } else {
                            printf("The file %s was deleted.\n", event->name);
                        }
                    } else if (event->mask & IN_MODIFY) {
                        if (event->mask & IN_ISDIR) {
                            printf("The directory %s was modified.\n", event->name);
                        } else {
                            printf("The file %s was modified.\n", event->name);
                        }
                    }
                }
                pos += EVENT_SIZE + event->len;
            }
        }
    }

    inotify_rm_watch(fd, wd);
    close(epfd);
    close(fd);
    return 0;
}