Event driven programming

We have seen in several examples already that we can use threads to allow a server to serve multiple clients at the same time.

An alternative to using threads is to use multiplexing I/O to construct a server that is able to serve multiple clients using just a single thread. This style of programming is known as event-driven programming. In an event-driven application the application features a main loop called an event-loop. On each iteration of the loop the application checks to see if events have happened that require processing. If a new event is available the application will process it and then go back around to waiting for new events to arrive. If no events are currently available the main loop will block until an event arrives.

Using select()

In chapter two of the textbook the author demonstrates how to use the select() system call to watch a set of file descriptors and be notified automatically when any of the file descriptors has data available to read. Using this approach we can design a server that serves multiple clients at the same time by using a socket for each client and then just using select() to be notified when one of the clients has data ready to process.

Here is a nice example online that demonstrates how to use select() to create a single-threaded server that can handle multiple clients simultaneously.

epoll system

A more modern alternative to using select().

#include <sys/epoll.h>

int epoll_create(int size);

The size parameter must be greater than 0, and will be ignored. epoll_create() returns a file descriptor, or -1 if an error occured.

int epoll_ctl(int epfd, int op, int fd, 
              struct epoll_event *event);

Adds a file descriptor to the watch set for this epoll instance. Pass the file descriptor you received from epoll_create() as the first parameter. Pass a file descriptor that you want to watch as the third parameter.

Valid values for the op argument are:

EPOLL_CTL_ADD Add an entry to the watch list.
EPOLL_CTL_MOD Change the settings associated with fd.
EPOLL_CTL_DEL Remove the fd from watch list.

The fourth parameter is a pointer to a structure

struct epoll_event {
  uint32_t events; /* Epoll events */
  epoll_data_t data; /* User data variable */
};

In the most common usage events will be EPOLLIN and data will be set to the file descriptor to watch. This sets up a watch for data available to read on the file descriptor.

int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

Wait for events on one or more watched file descriptors. Pass the epoll file descriptor returned by epoll_create() as the first parameter. The timeout value is the number of milliseconds that epoll_wait() will block before returning. Passing a value of -1 for the timeout value causes epoll_wait() to block until an event occurs. The events parameter is a pointer to a structure that will receive information about what events were generated.

maxevents is the maximum number of number of events you would like to receive, and must be greater then 0. epoll_wait() returns a count of the number of watched file descriptors that are available for the requested operation.

The events parameter is a pointer to an array of epoll_events structures. The number of entries in this array must be greater than or equal to the value passed for the maxevents parameter.

inotify system

#include <sys/inotify.h>

int inotify_init(void);

inotify_init() returns an integer file descriptor to use with this file watch.

int inotify_add_watch(int fd,const char *pathname,uint32_t mask);

Use with the file descriptor returned by inotify_init(). The path name can point to a file or a directory. If it points to a directory, the watch will apply to all files in the directory. You specify what events you want to watch by setting the mask.

Some examples of events:

IN_CREATE File/directory created in watched directory.
IN_DELETE File/directory deleted from watched directory.
IN_DELETE_SELF Watched file/directory was itself deleted.
IN_MODIFY File was modified.

inotify_add_watch() returns an integer watch descriptor.

To obtain file watch events, you read from the file descriptor returned by inotify_init(). If you call read() on a file descriptor provided by inotify_init(), the read will block until an event takes place. You can also use the epoll system to tell you when data is available on an inotify file descriptor.

The read will return one or more instances of the following structure:

struct inotify_event {
  int wd; /* Watch descriptor */
  uint32_t mask; /* Mask describing event */
  uint32_t cookie; /* Unique cookie associating related
                                  events (for rename(2)) */
  uint32_t len; /* Size of name field */
  char name[];  /* Optional null-terminated name */
 };

See the text for example code showing how to read one or more event structures.

Homework assignment

In this homework assignment we are going to use event-driven programming in the folder watcher system. In particular, we are going to set up a folder-watching system based on the inotify system. We will also be using epoll to wait for inotify events.

Once again, this assignment is going to start with a reading assignment from the textbook. You should start by reading the section titled "Monitoring File Events" at the end of chapter eight in the textbook, along with the "Event Poll" section of chapter four.

In this assignment you are going to set up a new version of the folder watcher system. Like the program you set up for assignment four, your folder watcher program will read a configuration file from /etc/fwd.conf that lists a set of folders to watch. Instead of setting up a separate thread for each folder to watch you will instead set up an inotify watch that watches for write events on files in each of the directories. To watch for write events in a directory, you only need to set up an inotify watch on the directory itself and set the watch to notify you when a create event or a write event takes place.

In the main loop of your program you will use an epoll mechanism to wait for events on the watches you set up. When you receive a notification that a write event has taken place you will need to log that event as usual to the fwd.log file. Part of the message you will write to the log file is the tag. You will need to set up a data structure that allows you to map inotify file descriptors or watch descriptors to the tags so you can write the correct tag to the log in case of an inotify event. Another part of the message is the time of the event. Since you should be receiving inotify events pretty close in time to when the file in question changed, it will suffice to just get the current time and write that to the log.

Just as in assignment four you will need to also set up your program to shut down gracefully if it receives the SIGINT or SIGTERM signal. This time around you will not be able to use sigwait() to wait for those signals, since you are going to be using the epoll mechanism instead to wait for inotify events. This means that you will need to install a signal handler to respond to these signals instead.

Here is an outline of how you can get your application to shut down gracefully in case of a signal:

// Global flag
int running;

// Signal handler
void handler(int sig) {
  running = 0;
}

int main() {
  running = 1;
  
  // Install signal handler
  // Open log file
  // Read config file and set up watches
  
  while(running) {
    // Call epoll_wait
    // Block signals
    // Process events
    // Unblock signals
  }
  // Close log file
}

If the shut down signal arrives while the main loop is blocked in the epoll_wait() call, epoll_wait() will return immediately with a count of -1. If you see a count of -1, which indicates that an error has taken place, you can check to see if errno is EINTR - this will confirm that the call to epoll_wait() was interrupted by the arrival of a signal.