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.