Monitor Linux file system events with inotif

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.

In this article, you will learn how to use inotify functions for a simple monitoring application. Download the sample code and compile it on your system to explore further.

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:

inotify_init

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

inotify_init1

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

inotify_add_watch

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.

inotify_rm_watch

removes a watched item from a watch list.

read

reads a buffer containing information about one or more events.

close

closes 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.

Notifications

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

1

2

3

4

5

6

7

8

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.

IN_ACCESS

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

IN_MODIFY

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

IN_ATTRIB

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

IN_CLOSE_WRITE

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

IN_CLOSE_NOWRITE

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

IN_CLOSE

A convenience mask that is the logical OR of the preceding two close events (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE).

IN_OPEN

A file or directory was opened.

IN_MOVED_FROM

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.

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.

IN_MOVE

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

IN_CREATE

Subdirectory or file was created in a watched directory.

IN_DELETE

Subdirectory or file was deleted in a watched directory.

IN_DELETE_SELF

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

IN_MOVE_SELF

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 (see the Download section) 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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

/* 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;

      printf("\n");

      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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

/* 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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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,

          mask);

      fflush (stdout);

      perror (" ");

    }

  else

    {

      watched_items++;

      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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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)

        {

          break;

        }

      else

        {

          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

1

2

3

4

5

6

7

8

9

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

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, inot_ev.name) +

                                  pevent->len;

      event = malloc (q_event_size);

      memmove (&(event->inot_ev), pevent, event_size);

      queue_enqueue (event, q);

      buffer_i += event_size;

      count++;

    }

  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. See the Download section for the complete code.

Listing 8. Processing events

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

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->inot_ev.name;

    }

  if ( event->inot_ev.mask & IN_ISDIR )

    {

      cur_event_file_or_dir = "Dir";

    }

  else

    {

      cur_event_file_or_dir = "File";

    }

  flags = event->inot_ev.mask &

    ~(IN_ALL_EVENTS | IN_UNMOUNT | IN_Q_OVERFLOW | IN_IGNORED );

  /* Perform event dependent handler routines */

  /* The mask is the magic that tells us what file operation occurred */

  switch (event->inot_ev.mask &

      (IN_ALL_EVENTS | IN_UNMOUNT | IN_Q_OVERFLOW | IN_IGNORED))

    {

      /* 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);

      break;

      /* 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);

      break;

      /* 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);

      break;

      /* 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);

      break;

      /* File open read-only was closed */

    case IN_CLOSE_NOWRITE:

      printf ("CLOSE_NOWRITE: %s \"%s\" on WD #%i\n",

          cur_event_file_or_dir, cur_event_filename, cur_event_wd);

      break;

      /* 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);

      break;

      /* 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,

              cur_event_cookie);

      break;

.

. (other cases)

.

      /* Watch was removed explicitly by inotify_rm_watch or automatically

         because file was deleted, or file system was unmounted.  */

    case IN_IGNORED:

      watched_items--;

      printf ("IGNORED: WD #%d\n", cur_event_wd);

      printf("Watching = %d items\n",watched_items);

      break;

      /* Some unknown message received */

    default:

      printf ("UNKNOWN EVENT \"%X\" OCCURRED for file \"%s\" on WD #%i\n",

          event->inot_ev.mask, cur_event_filename, cur_event_wd);

      break;

    }

  /* 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

1

2

3

4

5

6

7

8

9

10

11

12

13

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

ian@attic4:~/inotify-sample$

Watching dir1/ WD=1

Watching = 1 items

Watching dir1/dir2/ WD=2

Watching = 2 items

Watching dir1/dir2/file1 WD=3

Watching = 3 items

ian@attic4:~/inotify-sample$

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

1

2

3

4

5

6

7

8

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

file1

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

1

2

3

4

5

6

7

8

9

10

11

12

13

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

1

2

3

4

5

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

1

2

3

4

5

6

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

1

2

3

4

5

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

1

2

3

4

5

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

1

2

3

4

5

6

7

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

3 events queued

DELETE: Dir "dir2" on WD #1

DELETE_SELF: File "(null)" on WD #2

IGNORED: 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

1

2

3

4

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!

1

2

3

4

5

6

7

8

9

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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

IGNORED: 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

IGNORED: WD #1

Watching = 0 items

Terminating

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.

Security

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.

Conclusion

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.

Downloadable resources

Related topics

https://www.ibm.com/developerworks/library/l-inotify/

猜你喜欢

转载自blog.csdn.net/CaspianSea/article/details/83044574