All the events inotify can tell you about โ€” and how to use them

 

๐Ÿ”” Part 2 โ€“ inotify Events
All the events inotify can tell you about โ€” and how to use them

What Are inotify Events?

When you call inotify_add_watch(), you pass a bitmask telling the kernel which events to report. For example, IN_CREATE | IN_DELETE means “tell me when a file is created or deleted in this path”.

Events are split into two kinds:

  • Input โ€” bits you set in the mask argument of inotify_add_watch()
  • Output โ€” bits that appear in the mask field of the inotify_event structure you read back
  • Both โ€” bits used for both

Complete Event Reference Table
Event Bit Direction What Triggers It
IN_ACCESS In+Out File was read (e.g. someone called read() on it)
IN_ATTRIB In+Out File metadata changed: permissions, ownership, link count, extended attrs, UID/GID
IN_CLOSE_WRITE In+Out A file that was opened for writing was closed
IN_CLOSE_NOWRITE In+Out A file that was opened read-only was closed
IN_CREATE In+Out A file or directory was created inside a watched directory
IN_DELETE In+Out A file or directory was deleted from inside a watched directory
IN_DELETE_SELF In+Out The watched file or directory itself was deleted
IN_MODIFY In+Out File content was modified (e.g. write() call)
IN_MOVE_SELF In+Out The watched file or directory itself was renamed/moved
IN_MOVED_FROM In+Out A file was moved out of a watched directory (the source side of a rename)
IN_MOVED_TO In+Out A file was moved into a watched directory (the destination side of a rename)
IN_OPEN In+Out A file was opened
IN_ALL_EVENTS Input only Shorthand: monitors all of the above events at once
IN_MOVE Input only Shorthand for IN_MOVED_FROM | IN_MOVED_TO
IN_CLOSE Input only Shorthand for IN_CLOSE_WRITE | IN_CLOSE_NOWRITE
IN_DONT_FOLLOW Input only Do not dereference symlinks โ€” watch the symlink itself, not the target
IN_MASK_ADD Input only OR the new mask with the existing mask (instead of replacing it)
IN_ONESHOT Input only Monitor for one event only, then auto-remove the watch
IN_ONLYDIR Input only Fail with ENOTDIR if pathname is not a directory
IN_IGNORED Output only Watch was removed (by app or by kernel when file deleted/unmounted)
IN_ISDIR Output only The subject of the event is a directory, not a regular file
IN_Q_OVERFLOW Output only Event queue overflowed โ€” some events were lost
IN_UNMOUNT Output only The filesystem containing the watched file was unmounted

How Rename Events Work (IN_MOVED_FROM + IN_MOVED_TO)

When you rename a file (mv file1 file2), inotify generates two linked events:

Source Directory
/tmp/dir1/aaa.txt
IN_MOVED_FROM
wd=1, cookie=548
โŸถ mv โŸถ
Destination Directory
/tmp/dir2/bbb.txt
IN_MOVED_TO
wd=2, cookie=548

The cookie field has the same value in both events. This lets you match the FROM and TO events together to know it was a rename operation, not a delete + create.

Same directory rename: If you rename a file within the same directory (mv foo bar), both IN_MOVED_FROM and IN_MOVED_TO will have the same wd, but the same unique cookie still links them.

IN_DELETE vs IN_DELETE_SELF โ€” What’s the Difference?
Event What You Are Watching What Got Deleted
IN_DELETE A directory A file or subdirectory inside that watched directory
IN_DELETE_SELF A file or directory The watched object itself was deleted

Example: You watch /tmp/mydir. Someone deletes /tmp/mydir/notes.txt โ†’ you get IN_DELETE (name = “notes.txt”). Someone then deletes /tmp/mydir itself โ†’ you get IN_DELETE_SELF (no name field).

IN_ISDIR โ€“ How to Tell if the Event Is for a Directory

When an event occurs for a directory (not a file), the kernel sets the IN_ISDIR bit in the mask field of the returned event. You check it like this:

if (event->mask & IN_CREATE) {
    if (event->mask & IN_ISDIR)
        printf("New subdirectory created: %s\n", event->name);
    else
        printf("New file created: %s\n", event->name);
}

This is important because inotify is not recursive โ€” if a new subdirectory is created and you want to watch it too, you need to call inotify_add_watch() again for that new subdirectory.

Control Bits โ€” Not Events, But Behaviors

Some bits in the mask don’t describe events โ€” they modify how inotify_add_watch() behaves:

Flag What It Does
IN_DONT_FOLLOW Watch the symlink itself, not the file it points to. Useful for monitoring symlink creation/deletion.
IN_MASK_ADD If the path is already watched, OR the new events into the existing mask instead of replacing it.
IN_ONESHOT After the first event, automatically remove the watch. Good for “wait for one change then stop”.
IN_ONLYDIR Fail with ENOTDIR if the path is not a directory. Avoids race conditions.

๐Ÿ’ป Code Example 1 โ€“ Detect Config File Modification

A common daemon pattern: reload config when it changes, but only care about IN_CLOSE_WRITE (not every intermediate write).

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

#define BUF_SIZE 4096
#define CONFIG_FILE "/etc/myapp/config.conf"

void reload_config(void) {
    printf("Config changed! Reloading...\n");
    /* Your config reload logic here */
}

int main(void)
{
    int fd, wd;
    char buf[BUF_SIZE];
    ssize_t numRead;
    struct inotify_event *event;
    char *p;

    fd = inotify_init1(IN_CLOEXEC);
    if (fd == -1) { perror("inotify_init1"); exit(EXIT_FAILURE); }

    /*
     * Watch for IN_CLOSE_WRITE: triggered after the editor finishes
     * writing and closes the file. More reliable than IN_MODIFY
     * which fires on every single write() call.
     */
    wd = inotify_add_watch(fd, CONFIG_FILE, IN_CLOSE_WRITE);
    if (wd == -1) { perror("inotify_add_watch"); close(fd); exit(EXIT_FAILURE); }

    printf("Monitoring %s for changes...\n", CONFIG_FILE);

    for (;;) {
        numRead = read(fd, buf, BUF_SIZE);
        if (numRead <= 0) break;

        for (p = buf; p < buf + numRead; ) {
            event = (struct inotify_event *) p;

            if (event->mask & IN_CLOSE_WRITE)
                reload_config();

            p += sizeof(struct inotify_event) + event->len;
        }
    }

    close(fd);
    return 0;
}
Why IN_CLOSE_WRITE and not IN_MODIFY? Text editors like vim write temp files and rename them. Using IN_CLOSE_WRITE ensures you only reload when writing is completely done. IN_MODIFY would fire multiple times for a single save operation.

๐Ÿ’ป Code Example 2 โ€“ Track File Renames Using Cookie

Link IN_MOVED_FROM and IN_MOVED_TO events together using the cookie field.

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

#define BUF_SIZE 4096

int main(void)
{
    int fd, wd;
    char buf[BUF_SIZE];
    ssize_t numRead;
    struct inotify_event *event;
    char *p;

    /* Store the "from" side of a rename temporarily */
    uint32_t pending_cookie = 0;
    char pending_name[256] = {0};

    fd = inotify_init1(IN_CLOEXEC);
    if (fd == -1) { perror("inotify_init1"); exit(EXIT_FAILURE); }

    /* Watch /tmp/testdir for move events */
    wd = inotify_add_watch(fd, "/tmp/testdir", IN_MOVE);
    if (wd == -1) { perror("inotify_add_watch"); close(fd); exit(EXIT_FAILURE); }

    printf("Watching /tmp/testdir for renames...\n");
    printf("Try: mv /tmp/testdir/foo.txt /tmp/testdir/bar.txt\n\n");

    for (;;) {
        numRead = read(fd, buf, BUF_SIZE);
        if (numRead <= 0) break;

        for (p = buf; p < buf + numRead; ) {
            event = (struct inotify_event *) p;

            if (event->mask & IN_MOVED_FROM) {
                /* Store the old name and its cookie */
                pending_cookie = event->cookie;
                strncpy(pending_name, event->name, sizeof(pending_name) - 1);
            }

            if (event->mask & IN_MOVED_TO) {
                /* Match by cookie โ€” same rename operation */
                if (event->cookie == pending_cookie && pending_cookie != 0) {
                    printf("Renamed: %s  โ†’  %s\n", pending_name, event->name);
                    pending_cookie = 0;
                } else {
                    printf("Moved in from outside: %s\n", event->name);
                }
            }

            p += sizeof(struct inotify_event) + event->len;
        }
    }

    close(fd);
    return 0;
}

๐Ÿ’ป Code Example 3 โ€“ One-Shot Watch (Wait for One Event)

Use IN_ONESHOT to wait for exactly one event, then stop watching. Useful in scripts or one-time checks.

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

#define BUF_SIZE 4096

int main(int argc, char *argv[])
{
    int fd, wd;
    char buf[BUF_SIZE];
    ssize_t numRead;
    struct inotify_event *event;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file-to-watch>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    fd = inotify_init1(IN_CLOEXEC);
    if (fd == -1) { perror("inotify_init1"); exit(EXIT_FAILURE); }

    /*
     * IN_ONESHOT: after the first event fires, the watch is
     * automatically removed by the kernel.
     */
    wd = inotify_add_watch(fd, argv[1], IN_MODIFY | IN_ONESHOT);
    if (wd == -1) { perror("inotify_add_watch"); close(fd); exit(EXIT_FAILURE); }

    printf("Waiting for first modification to: %s\n", argv[1]);
    printf("(Will exit immediately after first change)\n");

    /* Block until one event arrives */
    numRead = read(fd, buf, BUF_SIZE);
    if (numRead > 0) {
        event = (struct inotify_event *) buf;
        if (event->mask & IN_MODIFY)
            printf("File was modified! Taking action...\n");
    }

    close(fd);
    return 0;
}
gcc -o oneshot oneshot.c
./oneshot /tmp/myfile.txt &
echo "hello" >> /tmp/myfile.txt
# Output: File was modified! Taking action...

Up Next: Reading inotify Events

Learn the inotify_event struct in detail and how to write a robust event read loop.

Part 3: Reading Events โ†’ โ† Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *