Asynchronous File I/O on Linux

by BradDaBug

Introduction

Synchonous I/O means that when you want to read or write something, perhaps with a call to a function called Read() or Write(), the function blocks, preventing execution from moving any further until the read or write is finished. This is how plain ol' file reads and writes typically work. You open a file then call read(), which fills a buffer with the data you want, and returns once everything is finished, letting you go on your way with a buffer filled with the data you want.

Asychronous I/O is the opposite. Instead of the read and write functions waiting until the requested operation is finished before they return, asynchronous I/O operations return immediately to your program while the read or write operation continues in the background.

So what good is this? It means that your program or game can continue on throwing stuff at the screen, updating input, scrolling the progress bar, whatever, all while the hard drive crunches on the data you wanted. You can also send multiple IO requests to the system, which then lets the operating system figure out the most efficient way to access all the data you wanted. Snazzy!
So your next question probably is "how do I do this?" I'm going to show you!

Right now the code presented in this tutorial only works on Linux, and maybe some other version of Unix, including MacOSX, but I haven't tested it. Windows code will be coming soon!

Getting started

You'll need to include these files to use AIO:

Note: I've found that on Linux the only necessary file is aio.h, but on MacOSX you'll need to include the other two. Also, I had to include sys/types.h before aio.h or else I'd get errors.

Also, the AIO API requires file descriptors. There is a round-about way to get the file descriptors for files opened with stdio or iostream, but the simplest way is to just use the open() system call. You'll need to include fcntl.h in order to use it.

Now then, there are three main AIO related functions we're going to be using in this simple example. They are:

aio_read()

aio_read() is the function where we tell the system what file we want to read, the offset to begin the read, how many bytes to read, and where to put the bytes that are read. All of this information goes into a aiocb structure, which looks like this:

There's plenty of other members inside this structure, but they aren't important to us right now. But it's still a good idea to zero out the structure before you use it. memset() would work fine for this.

aio_error()

aio_error() checks the current state of the IO request. Using this function you can find out of the request was successful or not. All you have to do is give it the address of the same aiocb structure that you gave aio_read(). The function returns 0 if the request completed successfully, EINPROGRESS if it's still working, or some other error code if an error occured.

By the way, you may be wondering if this means you'll have to create a different aiocb for each request. Well, you do. It should be obvious that if you muck around with an aiocb while a request is currently being fulfilled, bad things can happen. It should also be obvious that the buffer you give the aiocb will need to remain in existance the whole time the request is being fulfilled. So don't give it a pointer to a stack array and then jump out of the function. Bad things.

aio_return()

aio_return() checks the result of an IO request once you find out the request has been finished. If the request succeeded, this function returns the number of bytes read. If it failed then the function returns -1.

Code Dump!

#include <sys/types.h>
#include <aio.h>
#include <fcntl.h>
#include <errno.h>
#include <iostream>

using namespace std;

const int SIZE_TO_READ = 100;

int main()
{
	// open the file
	int file = open("blah.txt", O_RDONLY, 0);
	
	if (file == -1)
	{
		cout << "Unable to open file!" << endl;
		return 1;
	}
	
	// create the buffer
	char* buffer = new char[SIZE_TO_READ];
	
	// create the control block structure
	aiocb cb;
	
	memset(&cb, 0, sizeof(aiocb));
	cb.aio_nbytes = SIZE_TO_READ;
	cb.aio_fildes = file;
	cb.aio_offset = 0;
	cb.aio_buf = buffer;
	
	// read!
	if (aio_read(&cb) == -1)
	{
		cout << "Unable to create request!" << endl;
		close(file);
	}
	
	cout << "Request enqueued!" << endl;
	
	// wait until the request has finished
	while(aio_error(&cb) == EINPROGRESS)
	{
		cout << "Working..." << endl;
	}
	
	// success?
	int numBytes = aio_return(&cb);
	
	if (numBytes != -1)
		cout << "Success!" << endl;
	else
		cout << "Error!" << endl;
		
	// now clean up
	delete[] buffer;
	close(file);
	
	return 0;
}

To compile this code you'll most likely need to link to some external library. On Linux use -lrt, and on OSX it's -lc.

Hopefully this code should be pretty self explainatory. All it does is open a file, create a read request, then go into a busy loop until the request finishes. Obviously you wouldn't actually go into a busy loop while you waited for the request to finish in real life since that kind of defeats the purpose of asynchronous I/O, but you get the idea. Once the request is finished we find out whether it was successful or not.

You should see several "Working..." messages scrolling down the screen before the request is finally finished.

Conclusion

Asynchronous file I/O itself is pretty straight forward. Building a system around it might be a little bit more tricky, since you can't exactly drop it into an existing system without making some modifications. Those modifications could be something simple, like a manager that uses callbacks to notify objects when their requested operations are finished. Or you could go all out and build a multithreaded system that takes advantage of dual core machines for silky smooth lag free on-the-fly loading. It's up to you.