Digital Developer Conference: Cloud Security 2021 – Build the skills to secure your cloud and data Register free

Monitor Linux file system events with inotify

Introducing inotify

File system event monitoring is essential for many types of programs ranging from file managers to security tools. Since the Linux 2.6.13 kernel, Linux has included inotify, which allows a monitoring program to open a single file descriptor and watch one or more files or directories for a specified set of events, such as open, close, move/rename, delete, create or change attributes. Some enhancements have been made in later kernels, so check your kernel level before depending on those features.

A little history

Before inotify there was dnotify. Unfortunately, dnotify had limitations that left users hoping for something better. Some of the advantages of inotify are:

  • Inotify uses a single file descriptor, while dnotify requires opening one file descriptor for each directory that you intend to watch for changes. This can be very costly when you are monitoring several directories at once, and you may even reach a per-process file descriptor limit.
  • The file descriptor used by inotify is obtained using a system call and does not have an associated device or file. With dnotify, the file descriptor pins the directory, preventing the backing device to be unmounted, a particular problem with removable media. With inotify, a watched file or directory on a file system that is unmounted generates an event, and the watch is automatically removed.
  • Inotify can watch files or directories. Dnotify monitors directories, and so programmers had to keep stat structures or an equivalent data structure reflecting the files in the directories being watched, then compare those with the current state after an event occurred in order to know what happened to the entry in the directory.
  • As noted above, inotify uses a file descriptor, allowing programmers to use standard select or poll functions to watch for events. This allows for efficient multiplexed I/O or integration with Glib’s mainloop. In contrast, dnotify uses signals, which programmers often find more difficult or less than elegant. Signal-drive I.O notification was also added to inotify in kernel 2.6.25.

The API for inotify

Inotify provides a simple API that uses minimal file descriptors and allows fine granularity of monitoring. Communication with inotify is established through a system call. The available functions are as follows:

is the system call that creates an inotify instance and returns a file descriptor referring to the instance.

is similar to inotify_init with additional flags. If the flags are not specified, it behaves the same as inotify_init.

adds a watch for a file or directory and specifies which events are to be watched. Flags control whether events should be added to an existing watch, whether the watch should be done only if the path represents a directory, whether symbolic links should be followed or not, and whether the watch is a one-shot watch that should be stopped after the first event.

removes a watched item from a watch list.

is a buffer containing information about one or more events.

is the file descriptor, and removes any watches still remaining on that descriptor. When all file descriptors for an instance are closed, the resources and underlying object are freed so the kernel can reuse them.

So, a typical monitoring program will do the following:

  1. Use inotify_init to open a file descriptor
  2. Add one or more watches
  3. Wait for events
  4. Process events, then return to wait for more
  5. When no more watches are active or upon some signal, close the file descriptor, clean up, and exit.

In the next section, you’ll see the events you can watch, and how they work in our sample program. Finally you’ll see how the event monitoring works.


When your application reads a notification, a sequence of one or more events is read into a buffer you provide. Events are returned in a variable length structure as shown in Listing 1. If the amount of data fills your buffer, you may need to handle the case of partial event information or a partial name for the last entry.

Listing 1. The event structure for inotify

struct inotify_event
  int wd;               /* Watch descriptor.  */
  uint32_t mask;        /* Watch mask.  */
  uint32_t cookie;      /* Cookie to synchronize two events.  */
  uint32_t len;         /* Length (including NULs) of name.  */
  char name __flexarr;  /* Name.  */

Note that the name field is present only if the watched item is a directory and the event is for an item in the directory, other than the directory itself. The cookie is used to correlate an IN_MOVED_FROM event with the corresponding IN_MOVED_TO event if both relate to items being watched. The event type is returned in the mask field, along with flags that may be set by the kernel. For example, the IN_ISDIR flag is set by the kernel if the event is for a directory.

Events you can watch

There are several events you can watch. Some, such as IN_DELETE_SELF apply only to the item being watched, while others such as IN_ATTRIB or IN_OPEN may apply to the watched item, or if that item is a directory, to a directory or file within it.

The watched item or an entry in a watched directory was accessed. For example, an open file was read.

The watched item or an entry in a watched directory was modified. For example, an open file was updated.

The metadata changed on the watched item or an entry in a watched directory . For example, timestamps or permissions were changed.

A file or directory that had been open for write was closed.

A file or directory that had been open read-only was closed.

A convenience mask that is the logical OR of the preceding two close events.

A file or directory was opened.

A watched item or an entry in a watched directory was moved from the watched location. The event also includes a cookie to allow you to correlate IN_MOVED_FROM and IN_MOVED_TO.

A file or directory was moved to a watched location. The event includes a cookie as for IN_MOVED_FROM. If a file or directory is simply renamed, you would see both events. If it was moved to or from a location that you are not watching, you will see only one event. If you move or rename a watched item, the watch continues. See IN_MOVE-SELF below.

A convenience mask that is the logical OR of the preceding two move events ( IN_MOVED_FROM | IN_MOVED_TO).

Subdirectory or file was created in a watched directory.

Subdirectory or file was deleted in a watched directory.

The watched item itself was deleted. The watch is terminated and you will also receive an IN_IGNORED event.

The watched item itself was moved.

In addition to the event flags, there are several other flags that you can find in the inotify header (/usr/include/sys/inotify.h). For example, if you only want to watch for the first event, you can set the IN_ONESHOT flag when you add the watch.

A simple inotify application

Our sample application follows the above general logic. We use a signal handler to catch ctrl-c (SIGINT) and reset a flag (keep_running) so the application knows to terminate. The actual inotify calls are done in utility routines. Notice that we also create a queue so that events can be cleared from the underlying inotify object and then processed later. In a real application, you might want to do this in a different (and higher priority) thread than the one you use for processing the events. For this application, it simply illustrates the general principle. We use a very simple linked list of events where each of our queue entries consists of the original event plus space for a pointer to the next event in the queue.

The main program

The signal handler and main routine are shown in Listing 2. For this simple example, we set up a watch for each file or directory passed in on the command line, and we watch all events for each one, using the event mask IN_ALL_EVENTS. In a real application, you may want to track only file and directory creation or deletion events, so you might mask off open and close events as well as attribute changes. If you’re not interested in file or directory renames and moves, you could also mask off the various move events. See the man page of inotify for more details.

Listing 2. Sample main routine for inotify-test.c

/* Signal handler that simply resets a flag to cause termination */
void signal_handler (int signum)
  keep_running = 0;

int main (int argc, char **argv)
  /* This is the file descriptor for the inotify watch */
  int inotify_fd;

  keep_running = 1;

  /* Set a ctrl-c signal handler */
  if (signal (SIGINT, signal_handler) == SIG_IGN)
      /* Reset to SIG_IGN (ignore) if that was the prior state */
      signal (SIGINT, SIG_IGN);

  /* First we open the inotify dev entry */
  inotify_fd = open_inotify_fd ();
  if (inotify_fd > 0)

      /* We will need a place to enqueue inotify events,
         this is needed because if you do not read events
         fast enough, you will miss them. This queue is
         probably too small if you are monitoring something
         like a directory with a lot of files and the directory
         is deleted.
      queue_t q;
      q = queue_create (128);

      /* This is the watch descriptor returned for each item we are
         watching. A real application might keep these for some use
         in the application. This sample only makes sure that none of
         the watch descriptors is less than 0.
      int wd;

      /* Watch all events (IN_ALL_EVENTS) for the directories and
         files passed in as arguments.
         Read the article for why you might want to alter this for
         more efficient inotify use in your app.
      int index;
      wd = 0;
      for (index = 1; (index < argc) && (wd >= 0); index++)
      wd = watch_dir (inotify_fd, argv[index], IN_ALL_EVENTS);

      if (wd > 0)
      /* Wait for events and process them until a
         termination condition is detected
      process_inotify_events (q, inotify_fd);
      printf ("\nTerminating\n");

      /* Finish up by closing the fd, destroying the queue,
         and returning a proper code
      close_inotify_fd (inotify_fd);
      queue_destroy (q);
  return 0;

Opening the file descriptor using inotify_init

Listing 3 shows our simple utility function for creating an inotify instance and getting a file descriptor for it. The file descriptor is returned to the caller. If there is an error, the returned value is negative.

Listing 3. Using inotify_init

/* Create an inotify instance and open a file descriptor
   to access it */
int open_inotify_fd ()
  int fd;

  watched_items = 0;
  fd = inotify_init ();

  if (fd < 0)
      perror ("inotify_init () = ");
  return fd;

Adding a watch using inotify_add_watch

Once we have a file descriptor for the inotify instance, we need to add one or more watches. You use the mask to set particular events that you want to watch. In our example, we use the mask IN_ALL_EVENTS, which watches all available events.

Listing 4. Using inotify_add_watch

int watch_dir (int fd, const char *dirname, unsigned long mask)
  int wd;
  wd = inotify_add_watch (fd, dirname, mask);
  if (wd < 0)
      printf ("Cannot add watch for \"%s\" with event mask %lX", dirname,
      fflush (stdout);
      perror (" ");
      printf ("Watching %s WD=%d\n", dirname, wd);
      printf ("Watching = %d items\n", watched_items);
  return wd;

The event processing loop

Now that we have some watches set up, the next thing is to wait for events. We loop as long as there are some watches still set and our keep_running flag has not been reset by the signal handler. The loop waits for some event to happen, queues the available events, then processes the queue before returning to wait for more events. In a real application, you would probably post events to the queue in one thread, while processing them in another thread. The loop is shown in Listing 5.

Listing 5. The event processing loop

int process_inotify_events (queue_t q, int fd)
  while (keep_running && (watched_items > 0))
      if (event_check (fd) > 0)
      int r;
      r = read_events (q, fd);
      if (r < 0)
          handle_events (q);
  return 0;

Waiting for events

In our sample application, we wait indefinitely, and wake only if a watched event occurs or a signal interrupts processing. The code is shown in Listing 6.

Listing 6. Waiting for events or interrupts

int event_check (int fd)
  fd_set rfds;
  FD_ZERO (&rfds);
  FD_SET (fd, &rfds);
  /* Wait until an event happens or we get interrupted
     by a signal that we catch */
  return select (FD_SETSIZE, &rfds, NULL, NULL, NULL);

Reading the events

When an event occurs, we read as many events as will fit in a large buffer and then put them in a queue for our event handler to process. The sample code does not handle the case where there are more events available than will fit in our 16.384-byte buffer. It needs to be able to handle a partial event at the end of the buffer. Current limits for the name length should not be a problem, but good defensive programming might check that names could not overflow the buffer.

Listing 7. Reading and queuing events

int read_events (queue_t q, int fd)
  char buffer[16384];
  size_t buffer_i;
  struct inotify_event *pevent;
  queue_entry_t event;
  ssize_t r;
  size_t event_size, q_event_size;
  int count = 0;

  r = read (fd, buffer, 16384);
  if (r <= 0)
    return r;
  buffer_i = 0;
  while (buffer_i < r)
      /* Parse events and queue them. */
      pevent = (struct inotify_event *) &buffer[buffer_i];
      event_size =  offsetof (struct inotify_event, name) + pevent->len;
      q_event_size = offsetof (struct queue_entry, +
      event = malloc (q_event_size);
      memmove (&(event->inot_ev), pevent, event_size);
      queue_enqueue (event, q);
      buffer_i += event_size;
  printf ("\n%d events queued\n", count);
  return count;

Processing events

Finally! We have some events to process. For this application, we simply report what event occurred. If a name is present in the event structure, we report whether it is a file or directory. In the case of a move, we also report the cookie information that allows you to correlate move or rename events. Listing 8 shows part of the code, including the handling of some of the events.

Listing 8. Processing events

void handle_event (queue_entry_t event)
  /* If the event was associated with a filename, we will store it here */
  char *cur_event_filename = NULL;
  char *cur_event_file_or_dir = NULL;
  /* This is the watch descriptor the event occurred on */
  int cur_event_wd = event->inot_ev.wd;
  int cur_event_cookie = event->inot_ev.cookie;

  unsigned long flags;

  if (event->inot_ev.len)
      cur_event_filename = event->;
  if ( event->inot_ev.mask & IN_ISDIR )
      cur_event_file_or_dir = "Dir";
      cur_event_file_or_dir = "File";
  flags = event->inot_ev.mask &

  /* Perform event dependent handler routines */
  /* The mask is the magic that tells us what file operation occurred */
  switch (event->inot_ev.mask &
      /* File was accessed */
    case IN_ACCESS:
      printf ("ACCESS: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File was modified */
    case IN_MODIFY:
      printf ("MODIFY: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File changed attributes */
    case IN_ATTRIB:
      printf ("ATTRIB: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File open for writing was closed */
    case IN_CLOSE_WRITE:
      printf ("CLOSE_WRITE: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File open read-only was closed */
      printf ("CLOSE_NOWRITE: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File was opened */
    case IN_OPEN:
      printf ("OPEN: %s \"%s\" on WD #%i\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      /* File was moved from X */
    case IN_MOVED_FROM:
      printf ("MOVED_FROM: %s \"%s\" on WD #%i. Cookie=%d\n",
          cur_event_file_or_dir, cur_event_filename, cur_event_wd,
. (other cases)
      /* Watch was removed explicitly by inotify_rm_watch or automatically
         because file was deleted, or file system was unmounted.  */
    case IN_IGNORED:
      printf ("IGNORED: WD #%d\n", cur_event_wd);
      printf("Watching = %d items\n",watched_items);

      /* Some unknown message received */
      printf ("UNKNOWN EVENT \"%X\" OCCURRED for file \"%s\" on WD #%i\n",
          event->inot_ev.mask, cur_event_filename, cur_event_wd);
  /* If any flags were set other than IN_ISDIR, report the flags */
  if (flags & (~IN_ISDIR))
      flags = event->inot_ev.mask;
      printf ("Flags=%lX\n", flags);

This simple example is designed to illustrate how inotify works and what events you can watch. Your own needs will dictate which events you watch and how you handle them.

Example usage

In this section, we create a simple, two-level directory structure with a file in the directory and then run the sample application to illustrate some of the events that inotify can monitor. We will start the inotify sample program from a terminal session, but run it in background (using &) so that output from the program is interleaved with our commands. You could also run the program in one terminal window while running commands in one or more other windows. Listing 9 shows the creation of our sample directory structure and empty file, along with the output when we initially launch the sample program.

Listing 9. Creating the sample environment

ian@attic4:~/inotify-sample$ mkdir -p dir1/dir2
ian@attic4:~/inotify-sample$ touch dir1/dir2/file1
ian@attic4:~/inotify-sample$ ./inotify_test dir1/ dir1/dir2/ dir1/dir2/file1&
[2] 8733
Watching dir1/ WD=1
Watching = 1 items
Watching dir1/dir2/ WD=2
Watching = 2 items
Watching dir1/dir2/file1 WD=3
Watching = 3 items


In Listing 10, we show the output from listing the contents of dir2. The first event is reported for dir1, showing that something, namely dir2, in the directory being watched on watch descriptor 1 was opened. The second entry is for watch descriptor 2, showing that the item being watched (in this case, dir2) was opened. If you are watching many items in a directory tree, you will probably see this kind of dual output frequently.

Listing 10. Listing the contents of dir2

ian@attic4:~/inotify-sample$ ls dir1/dir2

4 events queued
OPEN: Dir "dir2" on WD #1
OPEN: Dir "(null)" on WD #2
CLOSE_NOWRITE: Dir "dir2" on WD #1
CLOSE_NOWRITE: Dir "(null)" on WD #2

In Listing 11, we add some text to file1. Note again the dual open, close, and modify events for the file and its containing directory. Note also that not all of the events were read at once. Our queuing routine was called three different times with two events each time. If you run the application again and do the same things each time, you may or may not repeat this particular behavior.

Listing 11. Adding text to file1

ian@attic4:~/inotify-sample$ echo "Some text" >> dir1/dir2/file1

2 events queued
OPEN: File "file1" on WD #2
OPEN: File "(null)" on WD #3

2 events queued
MODIFY: File "file1" on WD #2
MODIFY: File "(null)" on WD #3

2 events queued
CLOSE_WRITE: File "file1" on WD #2
CLOSE_WRITE: File "(null)" on WD #3

In Listing 12, we change the attributes of file1. Once again, we have dual output for the watched item and its containing directory.

Listing 12. Changing the attributes of a file

ian@attic4:~/inotify-sample$ chmod a+w dir1/dir2/file1

2 events queued
ATTRIB: File "file1" on WD #2
ATTRIB: File "(null)" on WD #3

Now let’s move file1 up a directory level into dir1. The resulting output is shown in Listing 13. This time we do not have dual entries. We actually have three entries, one for each directory and one for the file itself. Note that the cookie (569) allows us to correlate the MOVED-FROM event with the MOVED_TO event.

Listing 13. Moving file1 to dir1

ian@attic4:~/inotify-sample$ mv dir1/dir2/file1 dir1

3 events queued
MOVED_FROM: File "file1" on WD #2. Cookie=569
MOVED_TO: File "file1" on WD #1. Cookie=569
MOVE_SELF: File "(null)" on WD #3

Now let’s create a hard link from file1 to file2. Since the number of links to the inode changes, we have an ATTRIB event on file1, and we also have a CREATE event for file2.

Listing 14. Creating a hard link

ian@attic4:~/inotify-sample$ ln dir1/file1 dir1/file2

2 events queued
ATTRIB: File "(null)" on WD #3
CREATE: File "file2" on WD #1

Now let’s move file1 to our current directory, renaming it as file3. The current directory is not being watched, so there is no MOVED_TO event to correlate with the MOVED_FROM event.

Listing 15. Moving file1 to a directory that is not being watched

ian@attic4:~/inotify-sample$ mv dir1/file1 ./file3

2 events queued
MOVED_FROM: File "file1" on WD #1. Cookie=572
MOVE_SELF: File "(null)" on WD #3

At this point, dir2 is empty, so let’s remove it. Note that we get an IGNORED event for watch descriptor 2, so we are now watching only two items.

Listing 16. Removing dir2

ian@attic4:~/inotify-sample$ rmdir dir1/dir2

3 events queued
DELETE: Dir "dir2" on WD #1
DELETE_SELF: File "(null)" on WD #2
Watching = 2 items

Let’s remove file3. Note that we do not get an IGNORED event this time. Why not? And why do we get an ATTRIB event for file 3 (which was originally dir1/dir2/file1)?

Listing 16. Deleting file3

ian@attic4:~/inotify-sample$ rm file3

1 events queued
ATTRIB: File "(null)" on WD #3

Remember we created a hard link from file1 to file2. Listing 17 shows that we are still watching file2 on watch descriptor 3, even though there was no file 2 when we started!

Listing 17. We are still watching file2!

ian@attic4:~/inotify-sample$ touch dir1/file2

6 events queued
OPEN: File "file2" on WD #1
OPEN: File "(null)" on WD #3
ATTRIB: File "file2" on WD #1
ATTRIB: File "(null)" on WD #3
CLOSE_WRITE: File "file2" on WD #1
CLOSE_WRITE: File "(null)" on WD #3

So now let’s delete dir1 and watch the cascade of events, culminating with our program terminating itself because it is no longer watching anything.

Listing 18. Deleting dir1

ian@attic4:~/inotify-sample$ rm -rf dir1

8 events queued
OPEN: Dir "(null)" on WD #1
ATTRIB: File "(null)" on WD #3
DELETE_SELF: File "(null)" on WD #3
Watching = 1 items
DELETE: File "file2" on WD #1
CLOSE_NOWRITE: Dir "(null)" on WD #1
DELETE_SELF: File "(null)" on WD #1
Watching = 0 items


Possible uses of inotify

You can use inotify for many purposes. Here are some possibilities:

Performance monitoring
You may want to determine which files an applications opens most frequently. If you discover that a small file is repeatedly opened and closed, you might think about using an in-memory version, or changing the application so that the data is shared in some other way.

Meta information
You may want to log additional information about files, such as the original creation time, or the id of the user who last modified the file.

You may want to monitor all access to a particular file or directory for security reasons.

Our sample code watches for all events and reports all of them. In practice, you probably want to see only a specific subset of the events, according to your needs. You may also watch for different events on different watched items For example, you may want to watch for open and close events on files, but only creation or deletion events on directories. Whenever possible, you should limit your watches to the smallest set of events that interest you.


When applied to such areas as performance monitoring, debugging, and automation, inotify is a powerful, highly granular mechanism for monitoring Linux file systems. Use the sample code provided in this article to start writing your own applications that respond to or record file system events in real time with minimal performance overhead.