Linux inotify 文件监控

Linux 内核 2.6.13 以后,引入了 inotify 文件系统监控功能,通过 inotify 可以对敏感目录设置事件监听。这样的功能被也被包装成了一个文件监控神器 inotify-tools。

使用 inotify 进行文件监控的过程:

  1. 创建 inotify 实例,获取 inotify 事件队列文件描述符
  2. 为监控的文件逐一添加 watch,绑定 inotify 事件队列文件描述符,确定监控事件
  3. 使用 inotify 事件队列文件描述符读取产生的监控事件
  4. 完成以上操作后,关闭inotify事件队列文件描述符

除了以上的核心过程,一个文件监控系统还需要包含:监控文件的获取、监控事件的解析和数据补充。

inotify 文件事件监控核心部分所涉及的 API 如下(包含在 <sys/inotify.h> 中):

read 每次通过文件描述符读取的 inotify 事件队列中一个事件,事件的 mask 标记了文件发生的事件。inotify 事件的数据结构如下:

cpp 复制代码
/* 创建 inotify 实例,获取文件描述符 fd */
int inotify_init(void);//初始化一个新的 inotify 实例,返回一个与新的 inotify 事件队列关联的文件描述符
int inotify_init1(int flags);//如果flags为0,功能与inotify_init()相同

/* 添加 watch */
int inotify_add_watch(int fd, const char *pathname, uint32_t mask); //对于在pathname 中指定位置的文件,添加一个新的 watch,或者修改一个现有的 watch
int inotify_rm_watch(int fd, int wd);//从 inotify 中删除现有 watch 实例

/* 读取文件事件 */
ssize_t read(int fd, void *buf, size_t count);//尝试从inotify 事件队列关联的文件描述符fd读取多达count个字节到从buf开始的缓冲区中。成功时,返回读取的字节数(零表示文件结尾),文件位置按此数字前进。

/* 关闭文件描述符 */
int close(int fd);//关闭一个inotify 事件队列关联的文件描述符,使其不再引用任何文件

read 每次通过文件描述符读取的 inotify 事件队列中一个事件,事件的 mask 标记了文件发生的事件。inotify 事件的数据结构如下:

cpp 复制代码
struct inotify_event {
    int      wd;       /* 文件的监控描述符 */
    uint32_t mask;     /* 文件事件的掩码 */
    uint32_t cookie;   /* 重命名事件相关的唯一整数。对于所有其他事件类型,cookie 设置为 0 */
    uint32_t len;      /* 文件名称的长度 */
    char     name[];   /* 被监控的文件名称 */
};

inotify 事件 mask 的宏定义:

cpp 复制代码
#define IN_ACCESS         0x00000001        /* File was accessed.  */
#define IN_MODIFY         0x00000002        /* File was modified.  */
#define IN_ATTRIB         0x00000004        /* Metadata changed.  */
#define IN_CLOSE_WRITE    0x00000008        /* Writtable file was closed.  */
#define IN_CLOSE_NOWRITE  0x00000010        /* Unwrittable file closed.  */
#define IN_OPEN           0x00000020        /* File was opened.  */
#define IN_MOVED_FROM     0x00000040        /* File was moved from X.  */
#define IN_MOVED_TO       0x00000080        /* File was moved to Y.  */
#define IN_CREATE         0x00000100        /* Subfile was created.  */
#define IN_DELETE         0x00000200        /* Subfile was deleted.  */
#define IN_DELETE_SELF    0x00000400        /* Self was deleted.  */
#define IN_MOVE_SELF      0x00000800        /* Self was moved.  */

inotify 没有实现对目录的递归监控,需要自己添加这部分的功能,因此要判断文件类型,对于常规文件和目录文件分别进行处理。

Linux 下的文件元信息,可以通过 stat() 读取,st_mode 字段记录了文件的类型,取值 S_IFDIR、S_IFREG 分别表示目录文件和常规文件。

cpp 复制代码
struct stat {
    dev_t     st_dev;         /* ID of device containing file */
    ino_t     st_ino;         /* Inode number */
    mode_t    st_mode;        /* File type and mode */
    nlink_t   st_nlink;       /* Number of hard links */
    /* 此处省略部分数据 */
};

Linux 下 使用 readdir 打开目录获取目录信息,此函数返回一个 dirent 结构体,它的 d_type 字段记录了打开目录下的子文件的类型

cpp 复制代码
struct dirent {
    ino_t          d_ino;       /* Inode number */
    off_t          d_off;       /* Not an offset; see below */
    unsigned short d_reclen;    /* Length of this record */
    unsigned char  d_type;      /* Type of file; not supported by all filesystem types */
    char           d_name[256]; /* Null-terminated filename */
};

d_type 字段取值如下:

cpp 复制代码
enum
  {
    DT_UNKNOWN = 0,
# define DT_UNKNOWN            DT_UNKNOWN
    DT_FIFO = 1,
# define DT_FIFO               DT_FIFO
    DT_CHR = 2,
# define DT_CHR                DT_CHR
    DT_DIR = 4,
# define DT_DIR                DT_DIR //目录文件
    DT_BLK = 6,
# define DT_BLK                DT_BLK
    DT_REG = 8,
# define DT_REG                DT_REG //常规文件
    DT_LNK = 10,
# define DT_LNK                DT_LNK
    DT_SOCK = 12,
# define DT_SOCK               DT_SOCK
    DT_WHT = 14
# define DT_WHT                DT_WHT
  };

文件监控 demo:

cpp 复制代码
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
#include<iostream>

using std::string;

string event_str[12] =
{    
    "IN_ACCESS",        //文件被访问
    "IN_MODIFY",        //文件修改
    "IN_ATTRIB",        //文件元数据修改
    "IN_CLOSE_WRITE",
    "IN_CLOSE_NOWRITE",
    "IN_OPEN",
    "IN_MOVED_FROM",    //文件移动from
    "IN_MOVED_TO",      //文件移动to
    "IN_CREATE",        //文件创建
    "IN_DELETE",        //文件删除
    "IN_DELETE_SELF",
    "IN_MOVE_SELF"
};

class FileMonitor
{
public:
    void start_watch(int size, char *file_list[]);
    int watch_dir(const char *dir_path);
    FileMonitor();
    ~FileMonitor();
private:
    int fd;
};

FileMonitor::FileMonitor()
{
    fd = inotify_init1(IN_NONBLOCK);//创建inotify实例,返回与该实例相关的文件描述符 fd
    if (fd == -1) {
        std::cerr<<"Error: inotifiy initial failed !"<<std::endl;
        exit(EXIT_FAILURE);
    }
}

FileMonitor::~FileMonitor()
{
    if (fd > 0)
        close(fd);
}

void FileMonitor::start_watch(int size, char *file_list[])
{    
    struct stat file_info;
    int wd, file_type, event_list_len;
    struct inotify_event *event;
    char buf[8192] __attribute__ ((aligned(__alignof__(struct inotify_event))));
    
    for (int i=1; i < size; i++)
    {
        stat(file_list[i], &file_info);
        if (file_info.st_mode & S_IFREG)//普通文件直接添加 watch
        {
            wd = inotify_add_watch(fd, file_list[i], IN_ALL_EVENTS);
            if (wd == -1)
            {
                std::cerr<<"Error: cannot watch "<<file_list[i]<<" !"<<std::endl;
                exit(EXIT_FAILURE);
            }
        }
        if (file_info.st_mode & S_IFDIR) //目录文件需要遍历,为目录中的所有文件添加 watch
            watch_dir(file_list[i]);
    }

    //std::cout<<"start listening for events"<<std::endl;
    int event_len = sizeof(struct inotify_event);
    
    //读取inotify事件队列中的事件
    while (1)
    {
        if ((event_list_len = read(fd, buf, sizeof(buf))) > 0)
        for (char *ptr = buf; ptr < buf+event_list_len; ptr += event_len + event->len){
            event = (struct inotify_event *)ptr;
            //解析文件事件
            for (int i = 1; i < 12; i++){
                if ((event->mask >> i) & 1){
                    std::cout<<event->name<<": "<<event_str[i-1]<<std::endl;
                    /*
                     * event->name获取的是相对路径,获取绝对路径需要额外进行路径存储
                     *
                     * 这里可以针对敏感文件设置告警
                    */
                }
            }
        }
    }
}

int FileMonitor::watch_dir(const char *dir_path)
{
    
    DIR *dir;//打开的目录
    struct dirent *dir_container;//打开目录内容
    int wd, path_len, count = 0;
    string path = dir_path, path_str, prnt_path[1000];//当前目录、临时变量、子目录数组
    
    if ((dir = opendir(dir_path)) == NULL)
    {
        std::cerr<<"Error: cannot open directory '"<<dir_path<<"' !"<<std::endl;
        return -1;
    }
    
    wd = inotify_add_watch(fd, dir_path, IN_ALL_EVENTS);//为目录添加监控
    if (wd == -1)
    {
        std::cerr<<"Error: cannot watch '"<<dir_path<<"' !"<<std::endl;
        return -1;
    }
    
    while((dir_container = readdir(dir)) != NULL)
    {
        path_len = path.length();
        path_str = path[path_len-1];
        if (path_str.compare( "/") != 0)
                path += "/";
        path_str = path + (string)dir_container->d_name;//子文件绝对路径
        //std::cout<<"path: "<<path_str<<std::endl;
        if (dir_container->d_type == DT_REG)
        {
            inotify_add_watch(fd, (char *)path_str.c_str(), IN_ALL_EVENTS);//常规文件直接添加监控
            continue;
        }
        if (dir_container->d_type == DT_DIR
            && strcmp(".", dir_container->d_name) != 0
            && strcmp(dir_container->d_name,"..") != 0)
        {//目录文件加入子目录数组,等待递归遍历
            prnt_path[count] = path_str;
            count++;
        }
    }
    closedir(dir);
    while (count > 0){
        count--;
        watch_dir(prnt_path[count].c_str());//递归遍历目录,添加监控
    }
    return 0;
}

int main(int argc, char* argv[])
{
    if (argc < 2){
        std::cerr<<"Error: no watching file!"<<std::endl;
        exit(1);
    }
    FileMonitor monitor;
    monitor.start_watch(argc, argv);
    return 0;
}

编译执行:

bash 复制代码
c++ -o test -std=c++11 test.cpp && ./test /var/log

参考:

inotify(7) - Linux manual page

stat(2) - Linux manual page

readdir(3) - Linux manual page

dirent.h

sys_stat.h(0p) - Linux manual page

相关推荐
pk_xz12345643 分钟前
Shell 脚本中变量和字符串的入门介绍
linux·运维·服务器
小珑也要变强1 小时前
Linux之sed命令详解
linux·运维·服务器
Lary_Rock3 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
云飞云共享云桌面5 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮6 小时前
Linux 使用中的问题
linux·运维
dsywws7 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零8 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
小林熬夜学编程9 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
程思扬10 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节