Asynchronous I/O


The following examples illustrate the compexities of writing a program that is able to carry out some kind of computation while still being able to receive input from the user. They also show how to turn off keyboard echo, and avoid waiting for ENTER to be pressed at the end of a line, so single key commands work (games, editors, etc). The samples all work on rabbit (freeBSD 4.8) as they stand, and should require only minor pokings around for any other version of unix.


Basic Interrupts

(code here)

This program attempts to illustrate asynchronous communications through user-level interrupts. The idea is that control-C and the process "alarm clock" are easy to control, so they are used to simulate input coming from the user, and a long sleep() is used to simulate a long computation duirng which the input may arrive.

This line:
  signal(SIGINT, handle_ctrl_c);
starts "trapping" of control-Cs. It says that when a SIGINT signal (the official name of control-C) arrives, the function handle_control_c should be called asynchronously, instead of the default action (which is killing the program). SIGALRM is the name of the alarm clock signal.

This line:
  alarm(8);
schedules an alarm signal for 8 seconds in the future.

These lines:
    printf(".");
    fflush(stdout);
    sleep(5);
cause a line of dots to creep across the screen, one every five seconds, to mark "progress". Output is normally buffered, so without the fflush(), these dots would not be seen until a whole line has been produced.

The problem is that the delivery of most user-level interrupt signals cancels any ongoing sleep()s, so the illustration is a failure.


Keyboard Interrupts

(code here)

To improve realism, the SIGIO signal is also processed. This signal occurs when input or output could be done without blocking. To make the illustration more realistic, a few extras are added:

These lines:
  termios buf;
  tcgetattr(0, &buf);
  buf.c_lflag &= ~(ECHO | ICANON);
  buf.c_cc[VMIN]=1;
  buf.c_cc[VTIME]=0;
  tcsetattr(0, TCSAFLUSH, &buf);
turn off echo and "canonical" processing (dealing with backspaces etc) for the terminal.

These lines:
  int savedflags=fcntl(0, F_GETFL, 0);
  fcntl(0, F_SETFL, savedflags | O_ASYNC | O_NONBLOCK );
turn on asynchronous non-blocking mode for the terminal. Without them you would still have to press ENTER before input would be delivered to your program.

This line:
  fcntl(0, F_SETOWN, getpid());
is required because the SIGIO signal is only sent to the process that "owns" the terminal. It is not guaranteed (or even likely) that this program's process will be the terminal's official owner.

This line:
      printf(" IO(%c) ", c^32);
(especially the c^32 bit) converts capital letter to lower case and vice versa. A nasty little ascii trick, just to show that the input really was processed, and not just echoed strangely.

In this version, the problems are magnified. We still have the SIGALRM and SIGINT signals cancelling the sleep() and ruining the demo, plus now we have the fact that the system doesn't distinguish between input and output being complete. Every time anything is printed on the terminal (such as one of the dots), as well as every time a key is pressed, the SIGIO signal is sent.

Better Demonstration

(code here)

So this time, I abandoned the catching of control-Cs and alarm clock signals. As we are really receiving keyboard input, what's the point of simulating it with something simpler?

Also, instead of using sleep() to simulate a long computation, it is using a real long computation (A is the famous Ackermann's function). Sleep()s are cancelled when certain signals arrive, but real computations are of course allowed to continue, otherwise there would be no point in catching signals at all.

One problem remains. Progress is still indicated by printing dots across the screen. This still results in extra SIGIO signals being delivered.


Better Still

(code here)

The row of dots is no longer printed. Now we can more easily see the one remaining problem: no matter how many keys I press, the io_happened variable is simply set to indicate input is ready. For each repetition of the long computation, I still only receive a maximum of one input character.


Perfect Version

(code here)

Experimentation reveals that if you call getchar() in non-blocking mode when there is no input available, it simply returns zero. We can use this to make sure we receive all input that is available each time input availability is signalled.

So why bother with the signal at all? Just doing getchar() and seeing if the result is zero has the same effect. The reason is that getchar() takes much much longer to execute than just looking at one simple variable, so this way is much more efficient. Remember, in a real program this would have to happen very frequently for input to be processed reasonably quickly.

You might imagine that we could now go back to printing our row of dots to indicate progress, and we could: we now know how to tell if input was really there or not. But this would have to be done carefully. Producing output still causes a SIGIO, and we still have to do a getchar() to see if input was really ready. We would not want that to happen every time round a tight loop. Looking at a variable 10,000 times a second wouldn't be too bad, but calling getchar() 10,000 times a second would certainly have a noticable slowing effect. We would have to make sure that dots are only printed occasionally, if an indication of progress is really required.

The Real Problem

So we can use user-level interrupts to receive input from the keyboard without having to wait for it. Proper non-blocking, non-echoing, non-pre-processed input is possible.

But it is almost useless. The idea is that we could be performing a long computation, but still be able to react to input whenever it chose to arrive. We still have to keep looking at the special signal variable io_happened. In this last demo program, input is still not processed until the long computation is complete, even though the signal is received instantly.

To react to input at a proper rate, we would have to find some way to split the long computation (function A) up into small chunks that run for just a couple of milliseconds each, so that we would have a chance to look at the special variable between each chunk of computation, and that just isn't practical for a lot of real computations. Including this one.

select(): Alternative Approach

(code here)

Using the select() function, you can periodically "ask" if input is available from a particular file, without having to set up interrupt handling functions. The main inputs to select() are special data types called fd_sets (file descriptor sets). An fd_set is really just a big int, big enough to have one bit for every possible file you might want to check. If you want to check file N, you set the bit worth two-to-the-power-of-N in the fd_set. For example, to check on files 0, 3, and 4, your fd_set value would be 10011 in binary backwards, or 11001 in binary, or 25 in decimal. Special functions (actually macros) called FD_ZERO, etc, are provided to set a whole fd_set to zero, to set particular bits, and to test particular bits.

Select() is passed pointers to up to three fd_sets. The first one describes which input files you are asking about, and the second describes which output_files you are asking about. Unlike the SIGIO signal, select() distinguishes between input and output being ready.

Select() is normally a blocking call (it doesn't return until something is ready), but its final (optional, it can be NULL) parameter points to a struct that represents the maximum time to wait. Zero time of course makes it non-blocking. If input is already ready, it tells you, but otherwise won't wait for anything.

The result of select() is the number of files that are ready for use, and it modifies the fd_sets, clearing the bits that correspond to non-ready files.

In this first version, I just wanted to illustrate select() in its simplest environment, so did not change the terminal settings. You still have to press ENTER before any input is considered ready.


select(): Full Version

(code here)

This is simply the full version using select(), it turns off terminal echo, and accepts input as soon as a key is pressed, without waiting for ENTER. This is probably the sort of thing you would do if you were writing a visual editor to run under unix.