Linux 文件变动监控工具:原理、设计与实用指南(C/C++代码实现)

日常工作里,我们经常需要盯着某些文件或文件夹------比如看日志有没有新内容、配置文件有没有被改动。手动刷新查看既麻烦又容易错过关键变动,这时候一个自动监控工具就很实用了。今天要聊的,就是一款基于 Linux 系统特性开发的文件监控工具,它能实时捕捉文件的创建、删除、读写等操作,还能触发自定义命令,帮我们省不少事。

一、它到底能解决什么问题?

在讲技术之前,先搞清楚这个工具的核心用处。简单说,它就是个"文件管家",能帮我们做三件关键的事:

  1. 实时盯紧文件变动

    不管是有人新建了文件、删除了文件夹,还是修改了文件内容、调整了权限,它都能立刻察觉,并且把变动详情(比如时间、变动类型、文件路径)记录下来,不用我们手动去查。

  2. 按需选择监控类型

    不是所有变动都需要关注------比如只想知道文件有没有被修改,不想管权限变化。这时候可以精确选择监控的类型,避免无关信息干扰。

  3. 自动触发后续操作

    这是最实用的功能之一。比如监控到脚本文件被修改后,自动重新运行脚本;监控到日志文件有新内容,自动发送通知。不用我们在变动发生后手动执行下一步,形成"监控-响应"的自动化闭环。

二、核心原理:靠 Linux 的"专属工具"实现监控

这个工具能跑起来,全靠 Linux 内核提供的一个叫 inotify 的系统接口。可以把 inotify 理解成内核给用户程序开的"后门"------当文件发生变动时,内核会主动告诉监控程序"某个文件变了",不用程序自己反复去查(这种反复查询叫"轮询",效率很低)。

打个比方:如果把监控文件比作"等快递","轮询"就是每隔5分钟去楼下看一眼有没有快递;而 inotify 就是快递员到了会主动给你打电话,效率天差地别。

inotify 能识别的变动类型很丰富,正好对应工具的核心监控能力:

  • 新建文件/文件夹(IN_CREATE)
  • 删除文件/文件夹(IN_DELETE)
  • 读取文件内容(IN_ACCESS)
  • 修改文件内容(IN_MODIFY)
  • 调整文件权限(IN_ATTRIB)
  • 移动/重命名文件(IN_MOVED_FROM)

工具做的第一步,就是把我们要监控的文件/文件夹"注册"到 inotify 里,告诉内核:"这些文件有变动了记得通知我"。内核收到注册后,会给每个监控对象分配一个"监控描述符",后续变动就通过这个描述符传递给工具。

三、设计思路:怎么把"内核通知"变成"实用工具"?

inotify是一款用C语言编写的Linux平台可配置文件看门狗。inotify会监控一组文件或目录,并在所监视的资源每次发生变化时打印出日志事件。该看门狗可配置为监控任何类型的事件,包括文件创建和删除、文件移动、I/O和权限更改。此外,inotify可以在每次看门狗检测到更改时执行用户定义的命令,从而让您无需使用任何其他工具即可轻松构建复杂的管道。

cpp 复制代码
...
int main(int argc, char **argv) {
...
    struct option long_opts[] = {
        {"create",         no_argument, NULL, 'c'},
        {"delete",         no_argument, NULL, 'd'},
        {"move",           no_argument, NULL, 'm'},
        {"read",           no_argument, NULL, 'r'},
        {"write",          no_argument, NULL, 'w'},
        {"permission",     no_argument, NULL, 'p'},
        {"full",           no_argument, NULL, 'f'},
        {"exec",           required_argument, NULL, 'e'},
        {"no-timestamp",   no_argument, NULL,  0 },
        {"version",        no_argument, NULL, 'v'},
        {"help",           no_argument, NULL, 'h'},
        {NULL, 0, NULL, 0}
    };

    // Parse inotify options from command line
    while((opt = getopt_long(argc, argv, short_opts, long_opts, &opt_idx)) != -1) {
        switch(opt) {
            case 'c': mask |= IN_CREATE; break;
            case 'd': mask |= (IN_DELETE | IN_DELETE_SELF); break;
            case 'm': mask |= (IN_MOVE_SELF | IN_MOVED_FROM); break;
            case 'r': mask |= IN_ACCESS; break;
            case 'w': mask |= IN_MODIFY; break;
            case 'p': mask |= IN_ATTRIB; break;
            case 'f': mask = IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM |
                             IN_ACCESS | IN_MODIFY | IN_ATTRIB; break;
            case 'e': watchdog_cmd = optarg; break;
            case 0:
                if(!strcmp(long_opts[opt_idx].name, "no-timestamp")) {
                    is_timestamp_enabled = false;
                }
                break;
            case 'v': version(); return 0;
            case 'h': helper(argv[0]); return 0;
            default: helper(argv[0]); return 1;
        }
    }

    // 检索非选项参数的数量
    const int number_of_files = (argc - optind);

    if(number_of_files == 0) {
        puts("Error: provide at least one file/directory to watch");
        helper(argv[0]);
        return 1;
    }

    // 检查用户是否提供了足够的看门狗选项
    if(mask == 0) {
        puts("Error: provide at least one watchdog option");
        helper(argv[0]);
        return 1;
    }

    // 注册SIGINT信号处理程序
    signal(SIGINT, sigint_handler);

    // 初始化inotify API
    int fd = inotify_init1(IN_NONBLOCK);
    if(fd == -1) {
        perror("inotify_init1");
        exit(EXIT_FAILURE);
    }

    // 为每个监视描述符分配足够的内存
    int *wd = calloc(number_of_files, sizeof(int));
    if(wd == NULL) {
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    // Register each file into the watchlist
    int file_idx = optind;
    for(size_t idx = 0; file_idx < argc; file_idx++, idx++) {
        wd[idx] = inotify_add_watch(fd, argv[file_idx], mask);
        if(wd[idx] == -1) {
            fprintf(stderr, "Cannot watch '%s': %s\n", argv[file_idx], strerror(errno));
            exit(EXIT_FAILURE);
        }
    }

    nfds_t nfds = 1;
    struct pollfd fds[] = {
        { .fd = fd, .events = POLLIN }
    };

    while(!stop_signal) {
        int poll_num = poll(fds, nfds, -1);
        if(poll_num == -1) {
            if(errno == EINTR) {
                continue;
            }
            perror("poll");
            exit(EXIT_FAILURE);
        }

        if(poll_num > 0) {
            // Inotify 事件可用
            if(fds->revents & POLLIN) {
                handle_inotify_events(fd, wd, number_of_files, (argv + optind), is_timestamp_enabled, watchdog_cmd);


            }
        }
    }
    

    close(fd);
    free(wd);

    return 0;
}

static void handle_inotify_events(int fd, const int *wd, int wd_len, char **watched_files, const bool is_timestamp_enabled, const char *watchdog_cmd) {
    // 将inotify读取缓冲区与inotify_event结构对齐
    char inotify_read_buf[4096]
        __attribute__((aligned((__alignof__(struct inotify_event)))));
    const struct inotify_event *event;

    while(1) {
        // 从inotify文件描述符读取事件
        ssize_t len = read(fd, inotify_read_buf, sizeof(inotify_read_buf));
        if(len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        // 退出是否读取返回空
        if(len <= 0) {
            break;
        }

        //处理缓冲区中的每个事件
        for(char *ptr = inotify_read_buf; ptr < (inotify_read_buf + len); ptr += INOTIFY_EVENT_INC) {
            // 检索单个事件
            event = (const struct inotify_event*)ptr;

            // Set the event type
            watchdog_event we = E_UNDEF;
            if(event->mask & IN_CREATE) {
                we = E_CREATE;
            } else if(event->mask & (IN_DELETE | IN_DELETE_SELF)) {
                we = E_DELETE;
            } else if(event->mask & IN_MOVED_FROM) {
                we = E_MOVE;
            } else if(event->mask & IN_ACCESS) {
                we = E_READ;
            } else if(event->mask & IN_MODIFY) {
                we = E_WRITE;
            } else if(event->mask & IN_ATTRIB) {
                we = E_PERM;
            } else if(event->mask & IN_IGNORED) {
                //
                continue;
            }

            //将看门狗事件打印到标准输出
            uint8_t *file_name = NULL;
            size_t file_name_len;
            for(int i = 0; i < wd_len; i++) {
                if(wd[i] == event->wd) {
                    // 构建文件名
                    if(event->len) {
                        file_name_len = snprintf(NULL, 0, FILE_FMT, watched_files[i], event->name);
                        file_name = malloc((file_name_len+1) * sizeof(char));
                        if(file_name == NULL) {
                            perror("malloc");
                            exit(EXIT_FAILURE);
                        }

                        snprintf((char*)file_name, file_name_len+1, FILE_FMT, watched_files[i], event->name);
                    } else {
                        file_name_len = snprintf(NULL, 0, DIR_FMT, watched_files[i]);
                        file_name = malloc((file_name_len+1) * sizeof(char));
                        if(file_name == NULL) {
                            perror("malloc");
                            exit(EXIT_FAILURE);
                        }

                        snprintf((char*)file_name, file_name_len+1, DIR_FMT, watched_files[i]);
                    }

...
            }
            // 如果用户输入了命令,则执行该命令
            if(watchdog_cmd != NULL) {
                exec_command(watchdog_cmd);
            }

            free(file_name);
        }
        memset(inotify_read_buf, 0, sizeof(inotify_read_buf));
    }
}

static void get_timestamp(uint8_t *timestamp, const ssize_t timestamp_len) {
    time_t now = time(NULL);
    struct tm *timeinfo = localtime(&now);
    strftime((char*)timestamp, timestamp_len, "%Y-%m-%d %H:%M:%S", timeinfo);
}

static void exec_command(const char *cmd) {
    // 在新进程中执行命令
    pid_t pid = fork();
    
    if(pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if(pid == 0) { // 子进程
        // 标记化命令
        uint8_t **argv = tokenize_command(cmd);

        // 用新程序替换子进程的内存
        execvp((char*)argv[0], (char**)argv);

        // 如果execvp返回,则表示它执行失败了
        switch(errno) {
            case ENOENT: puts("Cannot execute command: no such file or directory"); break;
            case EACCES: puts("Cannot execute command: permission denied"); break;
            default: puts("Cannot execute command"); break;
        }

        //释放已分配的资源
        for (int i = 0; argv[i] != NULL; i++) {
            free(argv[i]);
        }
        free(argv);
        exit(EXIT_FAILURE);
    } else { // 父进程
        // 等待子进程退出
        int status;
        pid_t wpid = waitpid(pid, &status, 0);

        if (wpid == -1) {
            perror("waitpid");
            exit(EXIT_FAILURE);
        }

        // 当子进程(正常)退出时,记录错误信息
        if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
            printf("[Wolf] - Child process exited with status %d\n", WEXITSTATUS(status));
        }

        // 当子进程被信号终止时记录日志
        if (WIFSIGNALED(status)) {
            printf("[Wolf] - Child process wass terminated by signal %d\n", WTERMSIG(status));
        }
    }
}

static uint8_t **tokenize_command(const char *cmd) {
...

    // 为参数(和空终止符)分配足够的内存
    uint8_t **argv = malloc((argc + 1) * sizeof(uint8_t*));
    if(argv == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    // 重置命令字符串并再次进行标记化处理
    strcpy(cmd_dup, cmd);
    size_t idx = 0;
    token = strtok(cmd_dup, " ");
    while(token != NULL) {
        argv[idx] = (uint8_t*)strdup(token);
        if(argv[idx] == NULL) {
            perror("strdup");
            while(idx > 0) { free(argv[--idx]); }
            free(argv);
            free(cmd_dup);
            exit(EXIT_FAILURE);
        }
        idx++;
        token = strtok(NULL, " ");
    }

    // 以空字符结尾的argv,这是execvp所必需的
    argv[idx] = NULL;
    // 清除临时资源
    free(cmd_dup);

    return argv;
}

If you need the complete source code, please add the WeChat number (c17865354792)

c 复制代码
Syntax: './inotify [-c|-d|-m|-r|-w|-p|-f|-e] <PATH ...>'
options:
-c, --create              | Add a watchdog for file creation
-d, --delete              | Add a watchdog for file deletion
-m, --move                | Add a watchdog for file movements or file renaming
-r, --read                | Add a watchdog for reading events
-w, --write               | Add a watchdog for writing events
-p, --permission          | Add a watchdog for permissions changes
-f, --full                | Enable all the previous options
-e, --exec                | Execute a command when a watchdog detects a change
--no-timestamp            | Disable timestamp from watchdog output
-v, --version             | Show program version
-h, --help                | Show this helper

inotify的使用相当直接。它至少需要一个监视选项,并且至少需要一个文件/目录作为命令行参数进行监视。例如,要监视本地文件foo、bar以及目录src/的读取、写入和删除事件,请执行以下命令:

$> ./inotify -rwd foo bar src

此命令将在当前目录中添加一个监视器,用于监视当前路径上任何文件或目录生成的"读取"、"写入"和"删除"类型的事件。请注意,此命令不是递归的(有关更多信息,请参阅警告部分)。

此外,您还可以使用-f或--full选项,指示wolf为任何类型的事件添加监视器:

%> ./inotify --full $PWD

这相当于执行./inotify -cdmrwp $PWD。最后,您还可以使用--no-timestamp选项强制wolf禁用时间戳输出:

%> ./inotify -f --no-timestamp $PWD

这将产生以下输出:

c 复制代码
R '/home/marco/inotify  ' (dir)
R '/home/marco/inotify  /foo' (file)
D '/home/marco/inotify  /src' (dir)
R '/home/marco/inotify  ' (dir)
P '/home/marco/inotify  /a.out' (file)
W '/home/marco/inotify  /a.out' (file)

此外,如果您希望每次监视器检测到更改时执行自定义命令,可以使用-e,--exec选项来实现。例如,假设您在当前目录下有一个Python文件(foo.py),其内容如下:

c 复制代码
def square(x):
    return x ** 2

print(f"10^2 = {square(10)}")

并且你希望在保存到磁盘后立即对其进行持续评估。为此,你可以按照以下所述使用inotify:

$> ./inotify -w --exec 'python foo.py' .

每当看门狗检测到写入事件时,就会发出所提供的命令,从而自动评估程序,即:

c 复制代码
$> ./inotify -w --exec 'python foo.py' .
[2025-10-20 16:24:43] W 'foo.py' (file)
10^2 = 100
[2025-10-20 16:24:55] W 'foo.py' (file)
10^2 = 100
5^2 = 25
[2025-10-20 16:25:10] W 'foo.py' (file)
10^2 = 100
5^2 = 25
4^2 = 16

知道了靠 inotify 拿变动通知,接下来要解决的是:怎么把这些原始通知,变成普通人能看懂、能用上的功能?这里拆解几个关键设计思路:

1. 用"事件掩码"实现灵活监控

工具允许我们选择监控类型(比如只看修改、只看删除),背后靠的是"事件掩码"(一个二进制数字)。每种监控类型对应一个"掩码位",比如"监控修改"对应一个位,"监控删除"对应另一个位。

举个例子:如果我们选了"监控修改(W)"和"监控删除(D)",工具就会把这两个对应的掩码位"点亮",然后告诉 inotify:"只把这两种变动的通知发给我"。这样就能精准过滤掉不需要的信息,避免监控日志乱糟糟。

2. 用"非阻塞+轮询"处理通知

inotify 通知不是随时都有,工具总不能一直"发呆等通知"。这里用了两种技术结合:

  • 非阻塞模式:工具向 inotify 要通知时,如果暂时没有,不会"卡住",而是立刻返回"暂时没数据";
  • poll 轮询:工具会定期用 poll 函数"问"inotify:"有没有新通知?",有就处理,没有就继续等。

这种设计既不会让工具"卡死",又能及时捕捉到新通知,平衡了效率和响应速度。

3. 用"子进程"执行自定义命令

当监控到变动需要执行命令时(比如"修改后运行脚本"),工具不会直接执行命令,而是先"复制"一个自己(用 fork 函数创建子进程),让子进程去执行命令。

为什么要这么做?因为如果直接执行命令,万一命令卡住(比如脚本跑很久),整个监控工具都会跟着卡住,没法继续监控其他变动。用子进程执行,父进程(监控工具)可以等着子进程跑完,期间不影响正常监控,互不干扰。

而且子进程跑完后,父进程还会检查结果:如果命令执行失败(比如脚本报错),会把错误信息记下来,方便我们排查问题。

4. 处理边界情况:避免"无效监控"

实际使用中会有很多特殊情况,工具也做了对应的处理:

  • 监控对象被删除:如果监控的文件被删了,inotify 会自动取消这个文件的监控,工具不会一直盯着一个不存在的文件;
  • 没有权限的文件:如果对某个文件没有读写权限,工具会直接提示"没法监控",不会默默卡住;
  • 不支持递归监控:如果监控一个文件夹,只能看到这个文件夹下直接的变动,子文件夹里的变动看不到(这是 inotify 本身的限制,工具会在说明里提醒用户)。

四、相关领域知识点:理解背后的 Linux 基础

要彻底明白这个工具,需要了解几个 Linux 系统的基础概念,这些也是开发这类工具的核心知识:

1. 系统调用:用户程序和内核的"沟通语言"

工具里用到的 inotify_init1(初始化 inotify)、inotify_add_watch(注册监控对象)、fork(创建子进程)、poll(轮询通知),都是 Linux 的"系统调用"------也就是用户程序向内核"发请求"的接口。

普通程序(比如用 Python 写的脚本)不会直接用这些系统调用,而是通过编程语言的库包装后使用。但这类底层工具需要直接调用,才能获得更高的效率和控制力。

2. 文件描述符:Linux 里"一切皆文件"的体现

在 Linux 里,不管是真实的文件、文件夹,还是像 inotify 这样的"虚拟接口",都会被分配一个"文件描述符"(一个数字)。工具初始化 inotify 后,会拿到一个描述符,后续所有和 inotify 的交互(比如注册监控、读通知),都是通过这个描述符来完成的。

可以把文件描述符理解成"句柄"------拿着这个句柄,才能操作对应的"对象"(文件、接口等)。

3. 信号处理:优雅应对"强制退出"

当我们按 Ctrl+C 想关掉工具时,Linux 会给工具发一个 SIGINT 信号。工具里专门写了"信号处理函数",收到这个信号后,不会立刻崩溃,而是先清理资源(比如关闭 inotify 描述符、释放内存),再正常退出,避免留下"垃圾资源"。

五、总结:好用的工具都懂"平衡"

这款文件监控工具之所以实用,核心在于它在"底层能力"和"用户体验"之间做了很好的平衡:

  • 底层靠 inotify 保证效率,避免无意义的资源浪费;
  • 上层设计灵活的监控选项和命令触发功能,满足不同场景的需求;
  • 同时处理好边界情况(比如权限、删除),让用户不用花精力解决"异常问题"。

对于日常开发、运维来说,这类工具的价值在于"把人从重复劳动中解放出来"------不用再盯着文件等变动,不用在变动后手动执行命令,让自动化落地更简单。如果需要监控的场景比较简单,完全不用搭复杂的系统,用它就能快速实现"监控-响应"的闭环。

Welcome to follow WeChat official account【程序猿编码

相关推荐
网硕互联的小客服2 小时前
SSD和HDD存储应该如何选择?
linux·运维·服务器·网络·安全
lemon3106242 小时前
浪潮服务器装linux系统步骤
linux·运维·服务器
gugugu.2 小时前
Linux进程:进程状态
linux·运维·服务器
Wang's Blog3 小时前
Linux小课堂: Apache虚拟主机配置之基于IP与域名的服务器部署指南
linux·服务器·apache
Wang's Blog3 小时前
Linux小课堂: Apache服务在CentOS上的安装与基础配置指南
linux·centos·apache
广药门徒3 小时前
Linux 驱动开发中,主设备号和次设备号不同的两个驱动能否正常工作
linux·运维·驱动开发
踩坑小念3 小时前
进程 线程 协程基本概念和区别 还有内在联系
java·linux·jvm·操作系统
采坑先锋3 小时前
Docker环境离线安装-linux服务器
linux·服务器·docker
zhilin_tang3 小时前
在rk3568上架构纯c语言json脚本+webrtc服务音频设备播放设备程序
c语言·架构·json