如果你做过开发工具的热重载、文件同步,或者写过日志实时监控的脚本,大概率绕不开一个东西——inotify。
它是 Linux 内核给你的一个「文件系统变化订阅器」:注册一个文件描述符,告诉它盯着哪个目录,之后有任何风吹草动,它就通过这个 fd 把事件推送给你,免去了 busy polling。
本文聊聊它的原理、用法和踩坑点。
是什么
inotify(inode notify)是 Linux 内核提供的一种文件系统事件监控机制(kernel subsystem),自 Linux 2.6.13 起被引入。它允许应用程序实时监控文件或目录的变化,而无需通过轮询(polling)这种低效方式。
核心概念
inotify 基于**文件描述符(fd)**工作,应用程序通过一组系统调用与它交互:
| 系统调用 | 作用 |
|---|---|
inotify_init() | 创建一个 inotify 实例(旧接口,无 flags) |
inotify_init1() | 创建一个 inotify 实例,可指定 IN_NONBLOCK/IN_CLOEXEC |
inotify_add_watch() | 把某个文件/目录加入监控,返回一个 watch 描述符(wd) |
read() | 从 fd 读取发生的事件 |
inotify_rm_watch() | 移除监控 |
close() | 关闭 fd,自动清理所有监控 |
可监控的事件类型
常见的 event mask 包括:
IN_CREATE/IN_DELETE—— 文件/目录被创建、删除IN_MODIFY—— 文件内容被修改IN_MOVED_FROM/IN_MOVED_TO—— 移动(重命名)IN_ATTRIB—— 属性变化(权限、属主、时间戳等)IN_ACCESS/IN_CLOSE_WRITE/IN_OPEN—— 访问、打开、关闭IN_DELETE_SELF/IN_MOVE_SELF—— 被监控对象自身被删除/移动
还可以组合 IN_ALL_EVENTS、IN_ONLYDIR、IN_DONT_FOLLOW 等控制标志。
一个最小示例(C)
阻塞模式下,read() 会一直等到有事件才返回(教学场景最直观):
| |
关键点:
read()读出来的是变长的inotify_event结构体数组,按sizeof(struct inotify_event) + ev->len步进解析。- 当
ev->len == 0(如IN_DELETE_SELF这类被监控对象自身的事件)时name无效,要单独处理。 - 示例用阻塞模式 + 单次
read()演示「等到一个事件就处理」;生产代码里通常会包一层while循环持续读,或非阻塞 fd 配合poll/select/epoll做事件循环。
命令行工具
不想写代码也能用,inotify-tools 提供了两个好用的命令:
| |
排障、写临时脚本时非常顺手。
典型使用场景
- 开发工具热重载 —— 监控源码变化自动重启服务,比如
nodemon、vite、webpack-dev-server。 - 守护进程 / 同步工具 ——
rsync类增量同步、Dropbox / 网盘客户端的文件变动检测。 - 桌面环境 —— 文件管理器(Nautilus、Dolphin)自动刷新目录视图。
- 日志监控 —— 实时 tail 日志文件,新行写入即推送。
- 安全审计 / 入侵检测 —— 监控关键配置目录是否被篡改。
使用注意与限制
这一节是真正值钱的部分,实际项目里踩过的坑基本都在这。
不递归。 监控目录时,inotify 只报告直接子项的变化。要监控整棵目录树,你得自己递归 inotify_add_watch。好在主流库都帮你封装了这点。
资源上限。 受内核参数约束,单实例能加的 watch 数量是有限的:
| |
超出会报 ENOSPC(注意是 ENOSPC 不是 EMFILE,第一次遇到很容易懵)。在大项目里用 VSCode、WebStorm 时报这个错很常见,根因就是 watch 数被打满了。
事件可能被合并。 内核不保证每个字节写入都对应一个 IN_MODIFY 事件,短时间内多次修改可能被合并成一条。事件读取跟不上时队列还会溢出,此时内核会发一个 IN_Q_OVERFLOW 事件(mask 中带这个位、wd 为 -1)提醒你「有事件丢了」,这也是为什么在生产代码里要尽快消费事件,别让 read 长时间不读。
非 Linux 系统要适配。 macOS 用 kqueue / FSEvents,Windows 用 ReadDirectoryChangesW。跨平台时别直接裸调 inotify,用封装库更省心。
跨平台封装库
注意 C/C++ 这块:libinotify / inotify-tools 本质是 Linux inotify 的便利封装,并不跨平台。要真正在 macOS/Windows 之间统一,用 libuv、boost::asio 这类事件循环库更合适。
| 语言 | Linux 原生封装 | 跨平台方案 |
|---|---|---|
| C/C++ | libinotify、inotify-tools | libuv、boost::asio |
| Node.js | fs.watch | chokidar |
| Python | pyinotify | watchdog |
| Go | — | fsnotify/fsnotify(内部按平台分派) |
| Rust | — | notify |
理解了 inotify 的原理,再去看 chokidar、fsnotify、notify 这类库的实现就会顺畅很多——它们在不同平台上分别落到 inotify(Linux)、kqueue/FSEvents(macOS)、ReadDirectoryChangesW(Windows),抽象层之下就是这套机制。
小结
一句话:inotify 是 Linux 内核内置的「文件系统变化订阅器」——注册一个 fd,告诉它盯着哪里,之后的变化就以事件形式推送过来,免去了 busy polling。
它是几乎所有文件热重载、实时同步工具在 Linux 上的底层基石。