Think about these real-world situations:
- A file manager (like Nautilus) needs to refresh the folder view the moment you add or delete a file.
- A daemon (like nginx or sshd) wants to reload its configuration when you edit the config file โ without restarting the whole process.
- A backup tool wants to know immediately when any file in a directory changes so it can back it up.
- A build system (like Make or webpack’s watch mode) wants to recompile only when source files are edited.
The old way was to use a polling loop โ check every few seconds whether anything changed. That wastes CPU. The right way on Linux is inotify.
inotify is a Linux kernel subsystem that provides a notification mechanism for file system events. Instead of your program constantly asking “did anything change?”, the kernel tells your program when something changes.
It was added in Linux kernel 2.6.13 and replaced the older dnotify mechanism. The API lives in <sys/inotify.h>.
Using inotify always follows four steps. Think of it like subscribing to notifications:
| 1 | Create an inotify instance using inotify_init()Returns a file descriptor (fd) that represents this inotify instance. |
| 2 | Add watches using inotify_add_watch(fd, path, mask)Tell the kernel: “watch this file/directory for these events”. |
| 3 | Read events using read(fd, buf, size)The kernel fills your buffer with inotify_event structures whenever events happen. |
| 4 | Close the fd using close(fd)Automatically removes all watch items. Clean up when done. |
This call creates a new inotify instance inside the kernel and gives you a file descriptor to talk to it.
#include <sys/inotify.h>
int inotify_init(void);
/* Returns: file descriptor on success, -1 on error */
/* Newer variant with flags: */
int inotify_init1(int flags);
/* flags can be: IN_CLOEXEC, IN_NONBLOCK */
What is IN_CLOEXEC? If you fork() and exec() a child process, the inotify fd will be automatically closed in the child. This prevents accidental fd leaks.
What is IN_NONBLOCK? Normally read() on the inotify fd blocks until an event arrives. With IN_NONBLOCK, if no events are ready, read() returns immediately with EAGAIN.
inotify_init1(IN_CLOEXEC) instead of inotify_init() in production code. It avoids fd leaks across exec().This tells the kernel which file or directory to watch, and what events to watch for.
#include <sys/inotify.h>
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
/* Returns: watch descriptor (wd) on success, -1 on error */
fdโ the inotify instance fd frominotify_init()pathnameโ path to the file or directory to watchmaskโ bitmask of events to monitor (e.g.IN_CREATE | IN_DELETE)
It returns a watch descriptor (wd) โ a small integer that identifies this particular watch. When events arrive, the wd field in the event tells you which path triggered it.
Can you watch the same path twice? If you call inotify_add_watch() on a path that is already being watched by the same fd, it modifies the existing watch mask instead of creating a new watch. The same wd is returned.
Removes a watch that was previously added. This is optional โ all watches are removed automatically when you close the inotify fd.
#include <sys/inotify.h>
int inotify_rm_watch(int fd, uint32_t wd);
/* Returns: 0 on success, -1 on error */
When you remove a watch, the kernel generates an IN_IGNORED event for that watch descriptor to inform your program.
Here is a visual of how the inotify instance and its watch items are organized:
| โ |
|
One inotify instance (one fd) can have multiple watch items. Each watch item has a unique watch descriptor (wd).
This program watches /tmp/testdir and prints a message whenever a file is created inside it.
#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;
/* Step 1: Create inotify instance */
fd = inotify_init1(IN_CLOEXEC);
if (fd == -1) {
perror("inotify_init1");
exit(EXIT_FAILURE);
}
/* Step 2: Add a watch for /tmp/testdir, watch for file creation */
wd = inotify_add_watch(fd, "/tmp/testdir", IN_CREATE);
if (wd == -1) {
perror("inotify_add_watch");
close(fd);
exit(EXIT_FAILURE);
}
printf("Watching /tmp/testdir for new files...\n");
printf("Try: touch /tmp/testdir/hello.txt\n\n");
/* Step 3: Read events in a loop */
for (;;) {
numRead = read(fd, buf, BUF_SIZE);
if (numRead <= 0) {
perror("read");
break;
}
/* Process all events in the buffer */
for (p = buf; p < buf + numRead; ) {
event = (struct inotify_event *) p;
if (event->mask & IN_CREATE) {
if (event->len > 0)
printf("New file created: %s\n", event->name);
}
/* Move to next event in buffer */
p += sizeof(struct inotify_event) + event->len;
}
}
/* Step 4: Cleanup */
close(fd); /* This also removes all watches */
return 0;
}
How to compile and run:
mkdir -p /tmp/testdir
gcc -o watch_create watch_create.c
./watch_create &
# In another terminal:
touch /tmp/testdir/hello.txt
# Output: New file created: hello.txt
Watch two directories at the same time. Use the wd field to figure out which directory triggered the event.
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#define BUF_SIZE 4096
int main(void)
{
int fd, wd1, wd2;
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 two directories */
wd1 = inotify_add_watch(fd, "/tmp/dir1", IN_CREATE | IN_DELETE);
wd2 = inotify_add_watch(fd, "/tmp/dir2", IN_CREATE | IN_DELETE);
if (wd1 == -1 || wd2 == -1) {
perror("inotify_add_watch");
close(fd);
exit(EXIT_FAILURE);
}
printf("Watching /tmp/dir1 (wd=%d) and /tmp/dir2 (wd=%d)\n", wd1, wd2);
for (;;) {
numRead = read(fd, buf, BUF_SIZE);
if (numRead <= 0) break;
for (p = buf; p < buf + numRead; ) {
event = (struct inotify_event *) p;
/* Use wd to identify which directory */
const char *dir = (event->wd == wd1) ? "/tmp/dir1" : "/tmp/dir2";
const char *action = (event->mask & IN_CREATE) ? "CREATED" : "DELETED";
if (event->len > 0)
printf("[%s] %s: %s\n", dir, action, event->name);
p += sizeof(struct inotify_event) + event->len;
}
}
close(fd);
return 0;
}
How to test:
mkdir -p /tmp/dir1 /tmp/dir2
gcc -o watch_multi watch_multi.c
./watch_multi &
touch /tmp/dir1/fileA.txt # Output: [/tmp/dir1] CREATED: fileA.txt
touch /tmp/dir2/fileB.txt # Output: [/tmp/dir2] CREATED: fileB.txt
rm /tmp/dir1/fileA.txt # Output: [/tmp/dir1] DELETED: fileA.txt
This example shows how to dynamically add and remove a watch, and how to handle the IN_IGNORED event that is generated when a watch is removed.
#include <stdio.h>
#include <stdlib.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;
int eventCount = 0;
fd = inotify_init1(IN_CLOEXEC);
if (fd == -1) { perror("inotify_init1"); exit(EXIT_FAILURE); }
wd = inotify_add_watch(fd, "/tmp/testdir", IN_CREATE | IN_MODIFY);
if (wd == -1) { perror("inotify_add_watch"); close(fd); exit(EXIT_FAILURE); }
printf("Watching /tmp/testdir. Will auto-stop after 3 events.\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_IGNORED) {
/* Watch was removed โ kernel tells us via IN_IGNORED */
printf("Watch removed. Exiting.\n");
close(fd);
return 0;
}
if (event->len > 0)
printf("Event on: %s\n", event->name);
eventCount++;
if (eventCount >= 3) {
printf("3 events received. Removing watch...\n");
/* Remove the watch โ triggers IN_IGNORED event */
inotify_rm_watch(fd, wd);
}
p += sizeof(struct inotify_event) + event->len;
}
}
close(fd);
return 0;
}
| Point | Details |
|---|---|
| Not recursive | Watching a directory does NOT automatically watch subdirectories. You must call inotify_add_watch() separately for each subdirectory. |
| Directory events | When you watch a directory, you get events for files inside it AND for the directory itself. |
| select/poll/epoll | The inotify fd can be used with select(), poll(), and epoll. Useful for event-driven programs that watch multiple sources. |
| Kernel option | inotify must be compiled into the kernel (CONFIG_INOTIFY and CONFIG_INOTIFY_USER). It is enabled by default on all major distros. |
Learn all the event types โ IN_CREATE, IN_DELETE, IN_MODIFY, IN_MOVED_FROM, and many more.
