当前位置: 首页 > news >正文

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可以在每次看门狗检测到更改时执行用户定义的命令,从而让您无需使用任何其他工具即可轻松构建复杂的管道。

...
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 linewhile((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 APIint 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 watchlistint 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 typewatchdog_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)

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

这将产生以下输出:

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),其内容如下:

def square(x):return x ** 2print(f"10^2 = {square(10)}")

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

$> ./inotify -w --exec ‘python foo.py’ .

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

$> ./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【程序猿编码

http://www.dtcms.com/a/544238.html

相关文章:

  • 建站之星怎么用做视频解析网站犯法吗
  • LibreTV无广告观影实测:聚合全网资源,远程访问家庭影院新方案!
  • 仓颉中的 UTF-8 编码处理:从 DFA 解码、错误策略到流式与字素迭代的工程实战
  • 【React】打卡笔记,入门学习02:react-router
  • Latex 转 word 在线
  • 【OD刷题笔记】- 可以组成网络的服务器
  • 《算法闯关指南:优选算法--前缀和》--27.寻找数组的中心下标,28.除自身以外数组的乘积
  • linux arm64平台上协议栈发包报文长度溢出导致系统挂死举例
  • 深入理解 Rust `HashMap` 的哈希算法与冲突解决机制
  • 彩票网站开发做一个网站价格
  • 《C++ 继承》三大面向对象编程——继承:派生类构造、多继承、菱形虚拟继承概要
  • 医疗AI白箱编程:从理论到实践指南(代码部分)
  • Spring Cache 多级缓存中 hash 类型 Redis 缓存的自定义实现与核心功能
  • 福州建设人才市场网站山西网站推广
  • Spring Cache 多级缓存中 ZSet 类型 Redis 缓存的自定义实现与核心功能
  • 从开源到落地:SimpleBGC 三轴稳像平台全栈技术解析(上)
  • 51、STM32 与 ESP32 单片机全面对比:架构、性能与应用场景详解
  • NodeJs
  • 【面试题】缓存先删漏洞解决策略(示例代码)
  • 操作系统(7)虚拟内存-缓存工具-页命中和缺页(3)
  • 旧衣回收小程序的技术架构与商业落地:开发者视角的全链路解析
  • 丽水建设网站织梦网站发布的哪些产品和文章放在a文件可以吗
  • 南京网站设计公司济南兴田德润优惠吗泉州定制网站建设
  • 【设计模式笔记10】:简单工厂模式示例
  • wordpress多站批量发布wordpress 图像描述
  • 永宝网站建设招聘信息松江做移动网站
  • 云手机 基于云计算的虚拟手机
  • 广州网站制作哪家专业网站开发分为哪几种类型
  • server 2012 做网站常州市新北区建设与管理局网站
  • 百度的网站网址做网站所用的工具