A Solution: Asynchronous I/O

Let's try to address the issue of blocking calls in event-based applications in this lesson.

We'll cover the following

To overcome the issue of blocking calls discussed in the last lesson, many modern operating systems have introduced new ways to issue I/O requests to the disk system, referred to generically as asynchronous I/O. These interfaces enable an application to issue an I/O request and return control immediately to the caller before the I/O has completed; additional interfaces enable an application to determine whether various I/Os have completed.

An example: Mac’s API

For example, let us examine the interface provided on a Mac (other systems have similar APIs). The APIs revolve around a basic structure, the struct aiocb, or AIO control block in common terminology. A simplified version of the structure looks like this (see the manual pages for more information):

Press + to interact
struct aiocb {
int aio_fildes; // File descriptor
off_t aio_offset; // File offset
volatile void *aio_buf; // Location of buffer
size_t aio_nbytes; // Length of transfer
}

To issue an asynchronous read to a file, an application should first fill in this structure with the relevant information:

  • The file descriptor of the file to be read (aio_fildes).
  • The offset within the file (aio_offset) as well as the length of the request (aio_nbytes).
  • Finally the target memory location into which the results of the read should be copied (aio_buf).

After this structure is filled in, the application must issue the asynchronous call to read the file; on a Mac, this API is simply the asynchronous read API:

Press + to interact
int aio_read(struct aiocb *aiocbp);

This call tries to issue the I/O; if successful, it simply returns right away and the application (i.e., the event-based server) can continue with its work.

There is one last piece of the puzzle we must solve, however. How can we tell when an I/O is complete, and thus that the buffer (pointed to by aio_buf) now has the requested data within it?

One last API is needed. On a Mac, it is referred to (somewhat confusingly) as aio_error(). The API looks like this:

Press + to interact
int aio_error(const struct aiocb *aiocbp);

This system call checks whether the request referred to by aiocbp has completed. If it has, the routine returns success (indicated by a zero); if not, EINPROGRESS is returned. Thus, for every outstanding asynchronous I/O, an application can periodically poll the system via a call to aio_error() to determine whether said I/O has yet completed.

One thing you might have noticed is that it is painful to check whether an I/O has completed. If a program has tens or hundreds of I/Os issued at a given point in time, should it simply keep checking each of them repeatedly, or wait a little while first, or … ?

To remedy this issue, some systems provide an approach based on the interrupt. This method uses UNIX signals to inform applications when an asynchronous I/O completes, thus removing the need to repeatedly ask the system. This polling vs. interrupts issue is seen in devices too, as you will see (or already have seen) in the chapter on I/O devices.

In systems without asynchronous I/O, the pure event-based approach cannot be implemented. However, clever researchers have derived methods that work fairly well in their place. For example, Pai et al.“Flash: An Efficient and Portable Web Server” by Vivek S. Pai, Peter Druschel, Willy Zwaenepoel. USENIX ’99, Monterey, CA, June 1999. A pioneering paper on how to structure web servers in the then-burgeoning Internet era. Read it to understand the basics as well as to see the authors’ ideas on how to build hybrids when support for asynchronous I/O is lacking. describe a hybrid approach in which events are used to process network packets, and a thread pool is used to manage outstanding I/Os. Read their paper for details.

Get hands-on with 1400+ tech skills courses.