日常工作里,我们经常需要盯着某些文件或文件夹------比如看日志有没有新内容、配置文件有没有被改动。手动刷新查看既麻烦又容易错过关键变动,这时候一个自动监控工具就很实用了。今天要聊的,就是一款基于 Linux 系统特性开发的文件监控工具,它能实时捕捉文件的创建、删除、读写等操作,还能触发自定义命令,帮我们省不少事。
一、它到底能解决什么问题?
在讲技术之前,先搞清楚这个工具的核心用处。简单说,它就是个"文件管家",能帮我们做三件关键的事:
-
实时盯紧文件变动
不管是有人新建了文件、删除了文件夹,还是修改了文件内容、调整了权限,它都能立刻察觉,并且把变动详情(比如时间、变动类型、文件路径)记录下来,不用我们手动去查。
-
按需选择监控类型
不是所有变动都需要关注------比如只想知道文件有没有被修改,不想管权限变化。这时候可以精确选择监控的类型,避免无关信息干扰。
-
自动触发后续操作
这是最实用的功能之一。比如监控到脚本文件被修改后,自动重新运行脚本;监控到日志文件有新内容,自动发送通知。不用我们在变动发生后手动执行下一步,形成"监控-响应"的自动化闭环。
二、核心原理:靠 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【程序猿编码】