$RodHat_/search
Console Tips

inotify_add_watch(2): filesystem events without the poll loop

Published by · Updated

This article may contain Amazon affiliate links. I may earn a commission if you buy through them. Disclosure.

Editorial card: inotify_add_watch(2): filesystem events without the poll loop
Editorial card: RodHat
On this page · 5 sections
  1. Why IN_CLOSE_WRITE and not IN_MODIFY
  2. The name field
  3. What gets you at scale
  4. inotifywait for shell scripts
  5. FreeBSD

Twenty years of inotify(7) and people are still writing cron jobs to stat a file every 30 seconds. I have read this code in production. I deployed it myself once, in 2003, when inotify didn’t exist yet. Linux 2.6.13 shipped in August 2005. There is no longer an excuse.

Three syscalls: inotify_init1(2) opens the inotify file descriptor. inotify_add_watch(2) attaches a watch descriptor to a path. read(2) pulls queued events off the fd. The fd is a normal file descriptor. It works with epoll, select, and poll the same as any other.

#include <sys/inotify.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK);
    if (fd < 0) { perror("inotify_init1"); return 1; }

    int wd = inotify_add_watch(fd, "/etc/app.conf",
                               IN_CLOSE_WRITE | IN_MOVED_TO);
    if (wd < 0) { perror("inotify_add_watch"); return 1; }

    /* block until at least one event is queued */
    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(fd, &rfds);
    select(fd + 1, &rfds, NULL, NULL, NULL);

    char buf[4096]
        __attribute__((aligned(__alignof__(struct inotify_event))));
    ssize_t n = read(fd, buf, sizeof(buf));

    struct inotify_event *ev = (struct inotify_event *)buf;
    printf("wd=%d mask=0x%x name=%s\n",
           ev->wd, ev->mask, ev->len ? ev->name : "(none)");
    return 0;
}

Why IN_CLOSE_WRITE and not IN_MODIFY

IN_MODIFY fires on every write(2) call. Editors, config management tools, and anything that does an atomic write commits to a temp file and then renames it into place. The final path gets IN_MOVED_TO and the inode you were watching gets replaced. If you registered IN_MODIFY on the original path, you are now watching a deleted inode. The writer finished and walked away. Your daemon is still watching the corpse.

Watch IN_CLOSE_WRITE for direct writes (fires once when the last writer closes the fd) and IN_MOVED_TO for atomic renames. renameat2(2) covers why every sensible config writer does the rename dance, and the timerfd/signalfd/eventfd post covers integrating these event sources into a unified epoll loop.

The name field

struct inotify_event has a name member, but it is only populated when you watch a directory. When you watch a directory, name holds the basename of the file inside it that triggered the event. For a single-file watch, ev->len is zero and name is empty. This catches people when they add a directory watch and expect name to be the full path they passed.

One watch descriptor per inotify_add_watch call. If you watch twenty files, you have twenty descriptors mapped to the same inotify fd. The ev->wd field tells you which watch fired. Keep a table.

What gets you at scale

/proc/sys/fs/inotify/max_user_watches defaults to 8192 on older kernels and 65536 on more recent ones. Each watch is a kernel object. A service watching a whole source tree or thousands of log directories will hit this. The error is ENOSPC, which means nothing about disk space here, it means “no more watches.” Raise the limit in /etc/sysctl.d/ before deploying anywhere with a real workload.

IN_Q_OVERFLOW means the kernel dropped events. The queue bound is /proc/sys/fs/inotify/max_queued_events, default 16384. When it fills, the kernel queues exactly one IN_Q_OVERFLOW event and discards everything after. The fd stays readable, the watches remain registered, and your code will never see a single overflow unless it checks ev->mask & IN_Q_OVERFLOW. If it doesn’t check, it silently misses changes. This is a real failure mode in config-reload daemons.

IN_MOVED_FROM and IN_MOVED_TO events from the same rename share a cookie field. If your buffer only captured the first of the pair, you see a rename with no destination. Read the full available buffer before processing anything and match events by cookie before acting.

inotifywait for shell scripts

inotify-tools ships inotifywait(1), which wraps all of this without the boilerplate:

inotifywait -e close_write,moved_to /etc/app.conf && systemctl reload app

The C API earns its place when you have hundreds of paths to watch, need to integrate the fd into an existing epoll loop, or need to react inside a long-running daemon without spawning a subprocess. For a simple reload trigger, inotifywait is the right answer.

FreeBSD

FreeBSD uses kqueue(2) and kevent(2) with EVFILT_VNODE. You specify per-fd which operations to watch: NOTE_WRITE, NOTE_RENAME, NOTE_DELETE, NOTE_ATTRIB. The overflow behavior is explicit; there is no silent drop. The semantics are cleaner than inotify’s organic growth, though you lose the directory-as-a-whole watch and gain per-fd granularity instead.

If you need to intercept and block file opens rather than just observe them, that is fanotify(7) territory. inotify tells you what happened after the fact. fanotify can park the opener in the kernel and let you decide whether to allow it, which is what the AV and EDR crowd actually uses.

Sources

  1. inotify(7) Linux man page (man7.org)
  2. inotify_add_watch(2) Linux man page (man7.org)