Modern operating system kernels provide a mechanism called “asynchronous input/output (I/O)” where an execution thread submits an I/O request and then continues its execution while the I/O is performed in the background. Eventually the submitting thread (or perhaps another thread) checks or waits for the I/O to complete, and continues processing with the result of the operation. This mechanism avoids the inherent delays accompanying I/O requests that are serviced by long-latency devices, such as rotating disks and block storage devices. Typically such devices are mechanical in nature and must physically move to seek a track to read or write, which is orders of magnitude slower than the switching of electric current done by a processor in instruction-processing cycles.
If the storage system is simple (e.g., a block storage device) asynchronous I/O is simple to implement. However, if the storage system is complex (e.g., a file system), where a single I/O request from a thread translates to multiple interdependent requests to the device, implementation of asynchronous I/O becomes more difficult.
Currently, there are multiple ways to implement asynchronous I/O. One technique involves a state machine, where the kernel constructs a structure to track the state of the request, and submits the first of a series of I/O requests to the device. Each time a request completes, the kernel updates the structure and submits the next request. Eventually processing completes and the kernel signals the completion to the requesting thread. The drawback of this approach is that it is complicated to implement and hard to maintain.
Another technique involves passing the request to another thread, known as a helper thread. The requesting thread can then continue processing, while the helper thread processes the I/O request synchronously in the background. When the helper thread completes, it signals the requesting thread so that the requesting thread can then complete the I/O. While simple to implement, this approach incurs additional context switches, even in the case where the I/O could, in fact, be submitted immediately due to the needed data already being cached. These drawbacks reduce, and perhaps even nullify, the performance advantage gained by implementing asynchronous I/O in the first place.
Yet another technique involves running the process, submitting the first interdependent I/O in the series, and returning without waiting for the request to complete. When the I/O completes, the request is restarted from the beginning, so that the contents of the first I/O request are in the cache. Then, the thread proceeds to the second interdependent I/O in the series, submits the request, returns, and restarts from beginning when I/O completes, and so on. This approach avoids context switches, but is complex and requires extra work when restarting the process.