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
maskargument ofinotify_add_watch() - Output โ bits that appear in the
maskfield of theinotify_eventstructure you read back - Both โ bits used for both
| 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 |
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.
mv foo bar), both IN_MOVED_FROM and IN_MOVED_TO will have the same wd, but the same unique cookie still links them.| 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).
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.
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. |
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;
}
IN_CLOSE_WRITE ensures you only reload when writing is completely done. IN_MODIFY would fire multiple times for a single save operation.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;
}
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...
Learn the inotify_event struct in detail and how to write a robust event read loop.
