第 19 章 监控文件事件 (Monitoring File Events)

      +

      核心结论

      • inotify 是 Linux 2.6.13 起的文件事件监控机制:替代早期 dnotify;非递归——监控目录子树需显式添加每个目录的 watch。

      • 三步使用流程inotify_init() 创建实例(返回 fd)→ inotify_add_watch(fd, path, mask) 添加监控项(返回 watch descriptor)→ read(fd) 读取 inotify_event 结构。

      • inotify_event 字段wd(watch 描述符)、mask(事件位)、cookie(关联事件的配对标识)、len(name 字段长度)、name(触发事件的相对文件名,可选)。

      • 事件类型分两类:输入事件(如 IN_CREATEIN_MODIFYIN_DELETE 等)与控制标志(如 IN_DONT_FOLLOWIN_MASK_ADDIN_ONESHOTIN_ONLYDIR);输出事件(如 IN_IGNOREDIN_ISDIRIN_Q_OVERFLOWIN_UNMOUNT)。

      • 内核队列限制/proc/sys/fs/inotify/max_queued_eventsmax_user_instancesmax_user_watches——超限产生 IN_Q_OVERFLOW 事件(wd = -1)。

      • dnotify 已过时:基于信号机制,监控单位只能是目录,需为每个目录消耗 fd,事件信息粗;inotify 通过 fd 池、统一事件类型、精确文件名大幅超越。

      本章主旨

      本章介绍 Linux 的 inotify 文件事件监控 API——图形文件管理器、配置热加载、目录同步等场景的核心机制。读者应掌握三步 API 用法、inotify_event 字段语义、输入与输出事件类型、控制标志(IN_DONT_FOLLOW/IN_MASK_ADD/IN_ONESHOT/IN_ONLYDIR)的含义、事件合并与队列限制、以及与 dnotify 的对比。inotify 是 Linux 特有扩展(BSD 的对应物是 kqueue),不在 SUSv3 中。

      一、核心概念

      本章围绕 6 个核心概念展开:从 inotify 三步流程入手,到事件类型与控制标志、inotify_event 结构、队列与 /proc 限制、inotify vs dnotify 对比。

      概念 定义 + 重要性 实现提示

      inotify 三步 API

      inotify_init() 创建实例(返回 fd)→ inotify_add_watch(fd, path, mask) 加监控 → read(fd) 收事件;inotify_rm_watch(fd, wd) 移除监控;关闭 fd 自动移除所有 watch。

      §19.1/§19.2;inotify_init1(flags) 是 2.6.27 起的扩展版,支持 IN_CLOEXEC/IN_NONBLOCK;inotify fd 可用 select/poll/epoll/信号驱动 I/O 监控。

      事件类型:输入 vs 输出 vs 控制

      输入事件(IN_CREATEIN_MODIFYIN_DELETEIN_OPENIN_CLOSE_WRITE 等)是监控目标;控制标志(IN_DONT_FOLLOWIN_MASK_ADDIN_ONESHOTIN_ONLYDIR)修改 inotify_add_watch 行为;输出事件(IN_IGNOREDIN_ISDIRIN_Q_OVERFLOWIN_UNMOUNT)出现在 read 返回的 mask 中。

      §19.3;用户传给 inotify_add_watch 的 mask 是「输入事件 + 控制标志」;read 返回的 mask 只含「输入事件 + 输出事件」(不含控制标志)。

      inotify_event 结构

      struct inotify_event { int wd; uint32_t mask; uint32_t cookie; uint32_t len; char name[]; }cookie 关联 rename 类配对事件;name 仅目录内文件事件有值;len 含尾部填充字节。

      §19.4;单事件字节数 = sizeof(struct inotify_event) + len;buffer 至少 sizeof(inotify_event) + NAME_MAX + 1

      事件合并与队列

      队尾事件若与新事件 wd/mask/cookie/name 全相同则合并(不计入队列);这导致 inotify 不适合「需要精确计数」的场景;队列满触发 IN_Q_OVERFLOW

      §19.4/§19.5;ioctl(fd, FIONREAD, &n) 可查询当前可读字节数。

      内核参数 /proc/sys/fs/inotify/

      max_queued_events(默认 16384,每实例队列上限)、max_user_instances(默认 128,每 UID 实例数)、max_user_watches(默认 8192,每 UID watch 数)。

      §19.5;超限行为:队列满 → IN_Q_OVERFLOW;实例超限 → inotify_init 返回 EMFILE;watch 超限 → inotify_add_watch 返回 ENOSPC。

      inotify vs dnotify

      dnotify 基于信号(fcntl + F_NOTIFY);监控单位只能是目录;为每目录消耗 fd;事件信息粗;inotify 用 fd 池、可监控单文件、事件精确——dnotify 已废弃。

      §19.6;BSD 等价物是 kqueue;高层库如 FAM、Gamin 建立在 inotify 之上。

      二、详细笔记

      19.1 inotify 概述

      What:inotify 是 Linux 2.6.13 起提供的「文件/目录事件」监控机制;通过「内核事件队列 + 用户态 read」解耦监控与处理。

      Why:图形文件管理器需自动刷新、daemon 需监控配置文件变化、备份工具需感知目录变化——这些都需要「事件通知」而非「主动轮询」。

      How:三步流程:

      步骤 系统调用 关键返回

      1. 创建 inotify 实例

      inotify_init()

      inotify fd(文件描述符)

      2. 添加监控项

      inotify_add_watch(fd, path, mask)

      watch descriptor(非负整数)

      3. 读取事件

      read(fd, buf, size)

      一个或多个 inotify_event 结构

      监控完毕关闭 fd——所有 watch 自动移除。inotify_rm_watch(fd, wd) 显式移除某个 watch,触发 IN_IGNORED

      非递归——inotify 监控「目录」时只看到该目录内的直接条目变化;不会自动看到子目录内。要监控整棵子树必须显式遍历目录树(如 nftw)并对每个目录添加 watch;新增/删除子目录时需动态调整监控集。

      inotify fd 可与 select/poll/epoll 配合;2.6.25 起支持信号驱动 I/O。

      When:需要事件驱动而非轮询——文件管理器、配置热加载、目录同步、构建工具(监听源文件变化)。

      Example:核心三步示例:

      // 摘自《The Linux Programming Interface》第 19 章(Listing 19-1 简化)
      int inotifyFd = inotify_init();              /* 步骤 1 */
      int wd = inotify_add_watch(inotifyFd, argv[1], IN_ALL_EVENTS);  /* 步骤 2 */
      
      char buf[BUF_LEN];
      for (;;) {                                   /* 步骤 3 */
          ssize_t n = read(inotifyFd, buf, BUF_LEN);
          /* 遍历 buf 中的所有 inotify_event */
      }

      19.2 事件类型详解

      What:inotify 事件分三类——输入事件(监控目标)、控制标志(修改 inotify_add_watch 行为)、输出事件(在 read 返回的 mask 中告知额外状态)。

      Why:分类后用户能在「传给 inotify_add_watch 的 mask」与「read 收到的 mask」之间清晰区分哪些是配置、哪些是状态。

      How

      输入事件(用户传给 inotify_add_watch):

      事件 含义

      IN_ACCESS

      文件被 read

      IN_MODIFY

      文件被 write

      IN_ATTRIB

      文件元数据改变(权限、所有者、链接数、扩展属性、时间戳)

      IN_CLOSE_WRITE / IN_CLOSE_NOWRITE

      写/读打开的文件被 close

      IN_OPEN

      文件被 open

      IN_CREATE

      目录内文件/子目录创建

      IN_DELETE

      目录内文件/子目录删除

      IN_DELETE_SELF

      被监控对象自身被删除

      IN_MOVE_SELF

      被监控对象自身被 rename

      IN_MOVED_FROM / IN_MOVED_TO

      文件从监控目录移出/移入

      控制标志(仅传给 inotify_add_watch):

      标志 含义

      IN_DONT_FOLLOW

      不解引用符号链接(自 Linux 2.6.15)

      IN_MASK_ADD

      把 mask 按位或到当前 watch,而非替换

      IN_ONESHOT

      只监控一个事件,之后自动移除

      IN_ONLYDIR

      pathname 必须是目录,否则失败(防 race)

      输出事件(出现在 read 返回的 mask 中):

      事件 含义

      IN_IGNORED

      watch 被移除(应用显式 / 内核隐式:对象删除 / FS 卸载)

      IN_ISDIR

      事件主题是目录(与 IN_CREATE/IN_DELETE/IN_MOVED_* 配合)

      IN_Q_OVERFLOW

      队列溢出(wd = -1)

      IN_UNMOUNT

      被监控对象所在 FS 被卸载

      When

      • 通用场景:IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE_SELF

      • 文件管理器:加 IN_OPEN | IN_CLOSE_WRITE

        • 配置文件热加载:IN_MODIFY | IN_ATTRIB

        • 严格类型:用 IN_ONLYDIR 防止 race。

      Example:常见组合:

      // 文件管理器场景
      uint32_t mask = IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE_SELF
                    | IN_OPEN | IN_CLOSE_WRITE | IN_ATTRIB;
      inotify_add_watch(fd, dirpath, mask);

      cookie 配对与 rename:rename 跨监控目录时产生两条事件——源目录的 IN_MOVED_FROM 与目标目录的 IN_MOVED_TO;这两条事件的 cookie 字段相同——应用靠 cookie 关联「配对事件」。

      19.3 读取事件:inotify_event 与 buffer 布局

      What:每次 read(inotifyFd, buf, size) 返回一个 buffer,包含 1 个或多个 inotify_event 结构;事件之间紧凑排列。

      Why:理解 buffer 布局才能正确遍历——单事件字节数 = sizeof(struct inotify_event) + event→lenlen 包含 name 字符串 + 尾部填充字节。

      How

      // 摘自《The Linux Programming Interface》第 19 章
      struct inotify_event {
          int      wd;        /* watch descriptor */
          uint32_t mask;      /* 事件位 */
          uint32_t cookie;    /* 关联事件(rename 配对) */
          uint32_t len;       /* name 字段实际字节数 */
          char     name[];    /* 可选文件名 */
      };
      • wd:事件发生在哪个 watch 上;多监控项时用于查路径映射表。

      • mask:实际发生的事件位(输入 + 输出)。

      • cookie:rename 类配对事件共享相同 cookie。

      • len:name 占用的字节数(含填充);不是 strlen(name)。

      • name:被监控目录内文件事件有值;监控对象自身事件时 len = 0

      buffer 推荐大小:至少 sizeof(inotify_event) + NAME_MAX + 1——保证至少装下一个事件。read 返回的事件数 = min(可用事件数, buffer 能装下的事件数)

      buffer 太小 → read 返回 EINVAL(2.6.21+);之前返回 0。

      When

      • 高吞吐场景——buffer 用 10 * (sizeof(inotify_event) + NAME_MAX + 1) 一次读多个事件。

      • 调试「事件丢失」——用 ioctl(fd, FIONREAD, &n) 看当前队列长度;超长可能是处理太慢。

      • 单事件遍历步进:p += sizeof(struct inotify_event) + event→len

      Example:典型遍历代码:

      // 摘自《The Linux Programming Interface》第 19 章(Listing 19-1 简化)
      char *p = buf;
      while (p < buf + numRead) {
          struct inotify_event *event = (struct inotify_event *) p;
          displayInotifyEvent(event);
          p += sizeof(struct inotify_event) + event->len;
      }

      事件合并语义

      内核合并语义:新事件若与队尾事件的 wdmaskcookiename 完全相同,则新事件不入队——直接丢弃。

      后果:inotify 不适合「精确计数」场景(如「IN_MODIFY 触发了几次」);适合「事件已发生」通知。

      cookie 配对应用:rename 文件从 dir1/aaadir2/bbb 时:

      • dir1 收到 IN_MOVED_FROM,cookie = N,name = "aaa"。

      • dir2 收到 IN_MOVED_TO,cookie = N,name = "bbb"。

      应用保留「最近未匹配的 IN_MOVED_FROM」+ 后续 IN_MOVED_TO 配对,实现「移动文件」跟踪。

      19.4 队列限制与 /proc

      What:内核用三个 /proc 文件控制 inotify 资源使用;超限触发特定错误或 IN_Q_OVERFLOW 事件。

      Why:inotify 占用内核内存(事件队列、watch 表);没有限制会让恶意/有 bug 的进程耗尽资源。

      How

      /proc 文件 含义 默认

      max_queued_events

      每 inotify 实例队列最大事件数

      16384

      max_user_instances

      每 real UID 最大 inotify 实例数

      128

      max_user_watches

      每 real UID 最大 watch 数

      8192

      超限行为:

      • 队列满 → 产生 IN_Q_OVERFLOW 事件(wd = -1);后续事件丢弃直到队列有空间。

      • 实例超限 → inotify_init 返回 EMFILE

      • watch 超限 → inotify_add_watch 返回 ENOSPC

      When

      • 大规模监控——/etc/sysctl.conf 调高限制:fs.inotify.max_user_watches = 524288

      • 调试「为什么 inotify 突然不工作」——先看 /proc/sys/fs/inotify/*IN_Q_OVERFLOW 事件。

      19.5 dnotify 简史与对比

      What:dnotify 是 inotify 之前(Linux 2.4 起)的文件事件监控机制;用 fcntl(fd, F_NOTIFY, …​) 在目录 fd 上注册信号通知。

      Why:理解为什么 inotify 取代 dnotify——信号机制的问题(应用可能改变信号处置)、每目录消耗 fd、不能监控单文件、事件信息粗。

      How:dnotify 的核心缺陷:

      • 信号通知——主程序与库代码的信号处置冲突。

      • 监控单位只能是目录——单文件监控无法表达。

      • 每目录消耗一个 fd——监控 1000 个目录 = 1000 个 fd;FS 不能卸载。

      • 事件信息粗——只说「目录里发生了事件」,不说「哪个文件」。

      • 通知不可靠(race condition)。

      inotify 的优势:

      • 不用信号——用 fd 池 + read。

      • 可监控文件或目录。

      • 一个 fd 可承载多个 watch——不受单 fd 限制。

      • 事件含文件名、事件类型、cookie。

      • 提供 IN_DONT_FOLLOW 等精细控制。

      When:新代码用 inotify;维护老代码时才看 dnotify(F_NOTIFY 的 fcntl)。

      19.6 完整示例:demo_inotify

      What:第 19 章 Listing 19-1 demo_inotify——典型三步流程的可运行示例。

      Why:把 inotify_init、inotify_add_watch、read 三步完整连起来。

      How

      // 摘自《The Linux Programming Interface》第 19 章(Listing 19-1 简化)
      int inotifyFd = inotify_init();
      for (j = 1; j < argc; j++) {
          int wd = inotify_add_watch(inotifyFd, argv[j], IN_ALL_EVENTS);
          printf("Watching %s using wd %d\n", argv[j], wd);
      }
      
      #define BUF_LEN (10 * (sizeof(struct inotify_event) + NAME_MAX + 1))
      char buf[BUF_LEN];
      for (;;) {
          ssize_t numRead = read(inotifyFd, buf, BUF_LEN);
          if (numRead == 0) fatal("read() from inotify fd returned 0!");
          if (numRead == -1) errExit("read");
      
          char *p = buf;
          while (p < buf + numRead) {
              struct inotify_event *event = (struct inotify_event *) p;
              displayInotifyEvent(event);
              p += sizeof(struct inotify_event) + event->len;
          }
      }

      displayInotifyEvent 把 mask 的每个位翻译成可读字符串,并打印 name 字段。

      When:所有「事件循环」代码的模板——文件管理器、自动重载器、构建工具都用此模式。

      Example:运行示例(shell session 演示):

      $ ./demo_inotify dir1 dir2 &
      $ cat > dir1/aaa         # 创建文件
      Read 64 bytes from inotify fd
          wd = 1; mask = IN_CREATE
              name = aaa
          wd = 1; mask = IN_OPEN
              name = aaa
      $ mv dir1/aaa dir2/bbb   # rename 跨目录
      Read 64 bytes from inotify fd
          wd = 1; cookie = 548; mask = IN_MOVED_FROM
              name = aaa
          wd = 2; cookie = 548; mask = IN_MOVED_TO
              name = bbb
      $ mkdir dir2/ddd         # 创建目录
      Read 32 bytes from inotify fd
          wd = 1; mask = IN_CREATE IN_ISDIR
              name = ddd
      $ rmdir dir1             # 删除被监控目录
      Read 32 bytes from inotify fd
          wd = 1; mask = IN_DELETE_SELF
          wd = 1; mask = IN_IGNORED

      三、关键图表

      (本章无独立编号图表)

      inotify 三步流程 API 对照表
      调用 用途

      inotify_init() / inotify_init1(flags)

      创建 inotify 实例;返回 fd

      inotify_add_watch(fd, path, mask)

      加监控项;返回 wd

      inotify_rm_watch(fd, wd)

      移除监控项;触发 IN_IGNORED

      read(fd, buf, size)

      读 1+ 个 inotify_event

      ioctl(fd, FIONREAD, &n)

      查询当前可读字节数

      inotify 事件分类速查
      类别 事件位示例

      输入(监控目标)

      IN_ACCESS IN_MODIFY IN_ATTRIB IN_OPEN IN_CLOSE_WRITE IN_CLOSE_NOWRITE IN_CREATE IN_DELETE IN_DELETE_SELF IN_MOVE_SELF IN_MOVED_FROM IN_MOVED_TO

      控制(add_watch 行为)

      IN_DONT_FOLLOW IN_MASK_ADD IN_ONESHOT IN_ONLYDIR

      输出(read 返回)

      IN_IGNORED IN_ISDIR IN_Q_OVERFLOW IN_UNMOUNT

      简写

      IN_ALL_EVENTS IN_MOVE IN_CLOSE

      四、思维导图

      mindmap
        root((第 19 章 监控文件事件))
          三步 API
            inotify init 实例
            inotify add watch 加监控
            read 读事件
            inotify rm watch 移除
            close 自动清理
          事件分类
            输入 IN CREATE MODIFY
            输入 IN DELETE MOVE
            控制 IN DONT FOLLOW
            控制 IN MASK ADD ONESHOT ONLYDIR
            输出 IN IGNORED ISDIR
            输出 IN Q OVERFLOW UNMOUNT
          inotify event
            wd watch 描述符
            mask 事件位
            cookie rename 配对
            len name 长度
            name 可选文件名
            buffer 紧凑布局
          队列与限制
            max queued events 16384
            max user instances 128
            max user watches 8192
            IN Q OVERFLOW 事件
            事件合并语义
          非递归
            监控单层目录
            nftw 递归遍历
            新增子目录动态加 watch
          inotify vs dnotify
            dnotify 信号机制
            dnotify 每目录 fd
            dnotify 信息粗
            inotify fd 池
            inotify 精确事件
            FAM Gamin 高层库
          与 IO 多路复用
            select poll epoll
            信号驱动 IO
            ioctl FIONREAD

      五、重点与易错点

      1. inotify 非递归——监控目录只看到该目录内的直接条目变化;不会自动看到子目录内;要监控整棵子树必须显式遍历(如 nftw)并对每个目录加 watch,新子目录出现时动态加。

      2. 三步 API 流程inotify_init(返回 fd)→ inotify_add_watch(返回 wd)→ read;关闭 fd 自动移除所有 watch。

      3. 事件分三类——输入事件(监控目标)、控制标志(修改 add_watch 行为)、输出事件(出现在 read 返回的 mask 中);传给 add_watch 的 mask 与 read 收到的 mask 含义不同。

      4. inotify_event 字段语义wd 用于多 watch 时的路径查表;cookie 用于 rename 类配对事件;len 是字节数(含尾部填充),不是 strlen(name);单事件字节数 = sizeof(inotify_event) + event→len

      5. buffer 推荐大小——sizeof(inotify_event) + NAME_MAX + 1 保底;高吞吐场景用 10 倍以上一次 read 多事件。

      6. buffer 太小 → read 返回 EINVAL(2.6.21 起;之前返回 0);这是「编程错误」信号,应增大 buffer。

      7. 事件合并(coalescing)——新事件若与队尾事件 wd/mask/cookie/name 全相同则不计入队列;inotify 不适合「精确计数」场景。

      8. IN_Q_OVERFLOW 事件 wd = -1——队列满时触发;后续事件被丢弃直到有空间;高吞吐场景必须监控此事件。

      9. /proc/sys/fs/inotify/ 限制*:默认 max_queued_events=16384max_user_instances=128max_user_watches=8192;大规模监控需调高(如 sysctl -w fs.inotify.max_user_watches=524288)。

      10. cookie 关联 rename 配对事件——IN_MOVED_FROMIN_MOVED_TO 共享相同 cookie;应用保留「最近未匹配的 MOVED_FROM」+ 后续 MOVED_TO 配对。

      11. IN_IGNORED 不是「不重要」——它告知应用内核已经移除某个 watch(对象删除 / FS 卸载 / 应用显式移除);不监控此事件会导致 wd 长期保留在应用映射表中。

      12. IN_ONLYDIR 防 race——若 pathname 不是目录则 add_watch 失败(ENOTDIR);避免「加 watch 时是文件、之后被替换为目录」的 race。

        • IN_DONT_FOLLOW 用于符号链接监控——不加此标志,监控的是链接目标;加上后监控的是符号链接本身。

      13. IN_ONESHOT 用于「一次监控」——触发后自动移除 watch;典型场景:配置文件热加载只需第一次 IN_MODIFY。

      14. dnotify 已过时——基于信号机制,每目录消耗 fd,信息粗;新代码用 inotify;BSD 平台用 kqueue;高层库如 FAM/Gamin 建立在 inotify/kqueue 之上。

      15. 跨章衔接:第 18 章 nftw 用于递归遍历(在 inotify 非递归场景下用 nftw 枚举所有需要监控的目录);第 63 章 select/poll/epoll 用于多路复用 inotify fd;第 22 章信号与本章 dnotify 对比(dnotify 用信号通知)。