With I/O multiplexing, a process makes a system call (select() or poll()) in order to check whether I/O is possible on a file descriptor. With signal-driven I/O, a process requests that the kernel send it a signal when I/O is possible on a file descriptor. The process can then perform any other activity until I/O is possible, at which time the signal is delivered to the process. To use signal-driven I/O, a program performs the following steps:
fcntl(fd, F_SETOWN, pid);
Enable nonblocking I/O by setting the O_NONBLOCK
open file status flag.
flags = fcntl(fd, F_GETFL); /* Get current flags */ fcntl(fd, F_SETFL, flags | O_ASYNC | O_NONBLOCK);
Signal-driven I/O provides edge-triggered notification (). This means that once the process has been notified that I/O is possible, it should perform as much I/O (e.g., read as many bytes) as possible. Assuming a nonblocking file descriptor, this means executing a loop that performs I/O system calls until a call fails with the error EAGAIN
or EWOULDBLOCK
.
Several UNIX implementations, especially older ones, don’t define the O_ASYNC
constant for use with fcntl(). Instead, the constant is named FASYNC
, and glibc defines this latter name as a synonym for O_ASYNC
.
Example 63-3 provides a simple example of the use of signal-driven I/O. This program performs the steps described above for enabling signal-driven I/O on standard input, and then places the terminal in cbreak mode (Cooked, Cbreak, and Raw Modes), so that input is available a character at a time. The program then enters an infinite loop, performing the “work” of incrementing a variable, cnt, while waiting for input to become available. Whenever input becomes available, the SIGIO
handler sets a flag, gotSigio, that is monitored by the main program. When the main program sees that this flag is set, it reads all available input characters and prints them along with the current value of cnt. If a hash character (#
) is read in the input, the program terminates.
$ ./demo_sigio
cnt=37; read x
cnt=100; read x
cnt=159; read x
cnt=223; read x
cnt=288; read x
cnt=333; read #
Example 63-3. Using signal-driven I/O on a terminal
altio/demo_sigio.c
#include <signal.h> #include <ctype.h> #include <fcntl.h> #include <termios.h> #include "tty_functions.h" /* Declaration of ttySetCbreak() */ #include "tlpi_hdr.h" static volatile sig_atomic_t gotSigio = 0; /* Set nonzero on receipt of SIGIO */ static void sigioHandler(int sig) { gotSigio = 1; } int main(int argc, char *argv[]) { int flags, j, cnt; struct termios origTermios; char ch; struct sigaction sa; Boolean done; /* Establish handler for "I/O possible" signal */ sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = sigioHandler; if (sigaction(SIGIO, &sa, NULL) == -1) errExit("sigaction"); /* Set owner process that is to receive "I/O possible" signal */ if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) errExit("fcntl(F_SETOWN)"); /* Enable "I/O possible" signaling and make I/O nonblocking for file descriptor */ flags = fcntl(STDIN_FILENO, F_GETFL); if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) errExit("fcntl(F_SETFL)"); /* Place terminal in cbreak mode */ if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1) errExit("ttySetCbreak"); for (done = FALSE, cnt = 0; !done ; cnt++) { for (j = 0; j < 100000000; j++) continue; /* Slow main loop down a little */ if (gotSigio) { /* Is input available? */ /* Read all available input until error (probably EAGAIN) or EOF (not actually possible in cbreak mode) or a hash (#) character is read */ while (read(STDIN_FILENO, &ch, 1) > 0 && !done) { printf("cnt=%d; read %c\n", cnt, ch); done = ch == '#'; } gotSigio = 0; } } /* Restore original terminal settings */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1) errExit("tcsetattr"); exit(EXIT_SUCCESS); }altio/demo_sigio.c
We set the file descriptor owner using an fcntl() operation of the following form:
fcntl(fd, F_SETOWN, pid);
We may specify that either a single process or all of the processes in a process group are to be signaled when I/O is possible on the file descriptor. If pid is positive, it is interpreted as a process ID. If pid is negative, its absolute value specifies a process group ID.
On older UNIX implementations, an ioctl() operation—either FIOSETOWN
or SIOCSPGRP
—was used to achieve the same effect as F_SETOWN
. For compatibility, these ioctl() operations are also provided on Linux.
Typically, pid is specified as the process ID of the calling process (so that the signal is sent to the process that has the file descriptor open). However, it is possible to specify another process or a process group (e.g., the caller’s process group), and signals will be sent to that target, subject to the permission checks described in Section 20.5, where the sending process is considered to be the process that does the F_SETOWN
.
The fcntl() F_GETOWN
operation returns the ID of the process or process group that is to receive signals when I/O is possible on a specified file descriptor:
id = fcntl(fd, F_GETOWN); if (id == -1) errExit("fcntl");
A process group ID is returned as a negative number by this call.
The ioctl() operation that corresponds to F_GETOWN
on older UNIX implementations was FIOGETOWN
or SIOCGPGRP
. Both of these ioctl() operations are also provided on Linux.
A limitation in the system call convention employed on some Linux architectures (notably, x86) means that if a file descriptor is owned by a process group ID less than 4096, then, instead of returning that ID as a negative function result from the fcntl() F_GETOWN
operation, glibc misinterprets it as a system call error. Consequently, the fcntl() wrapper function returns -1, and errno contains the (positive) process group ID. This is a consequence of the fact that the kernel system call interface indicates errors by returning a negative errno value as a function result, and there are a few cases where it is necessary to distinguish such results from a successful call that returns a valid negative value. To make this distinction, glibc interprets negative system call returns in the range -1 to -4095 as indicating an error, copies this (absolute) value into errno, and returns -1 as the function result for the application program. This technique is generally sufficient for dealing with the few system call service routines that can return a valid negative result; the fcntl() F_GETOWN
operation is the only practical case where it fails. This limitation means that an application that uses process groups to receive “I/O possible” signals (which is unusual) can’t reliably use F_GETOWN
to discover which process group owns a file descriptor.
We now consider the details of when “I/O possible” is signaled for various file types.
For the read end of a pipe or FIFO, a signal is generated in these circumstances:
For the write end of a pipe or FIFO, a signal is generated in these circumstances:
A TCP connect() request completes; that is, the active end of a TCP connection entered the ESTABLISHED state, as shown in Figure 61-5 (page 1272). The analogous condition is not signaled for UNIX domain sockets.
New input is received on the socket (even if there was already unread input available).
The peer closes its writing half of the connection using shutdown(), or closes its socket altogether using close().
Output is possible on the socket (e.g., space has become available in the socket send buffer).
In applications that need to simultaneously monitor very large numbers (i.e., thousands) of file descriptors—for example, certain types of network servers—signal-driven I/O can provide significant performance advantages by comparison with select() and poll(). Signal-driven I/O offers superior performance because the kernel “remembers” the list of file descriptors to be monitored, and signals the program only when I/O events actually occur on those descriptors. As a result, the performance of a program employing signal-driven I/O scales according to the number of I/O events that occur, rather than the number of file descriptors being monitored.
To take full advantage of signal-driven I/O, we must perform two steps:
if (fcntl(fd, F_SETSIG, sig) == -1) errExit("fcntl");
The F_GETSIG
operation performs the converse of F_SETSIG
, retrieving the signal currently set for a file descriptor:
sig = fcntl(fd, F_GETSIG); if (sig == -1) errExit("fcntl");
(In order to obtain the definitions of the F_SETSIG
and F_GETSIG
constants from <fcntl.h>
, we must define the _GNU_SOURCE
feature test macro.)
Using F_SETSIG
to change the signal used for “I/O possible” notification serves two purposes, both of which are needed if we are monitoring large numbers of I/O events on multiple file descriptors:
The default “I/O possible” signal, SIGIO
, is one of the standard, nonqueuing signals. If multiple I/O events are signaled while SIGIO
is blocked—perhaps because the SIGIO
handler is already invoked—all notifications except the first will be lost. If we use F_SETSIG
to specify a realtime signal as the “I/O possible” signal, multiple notifications can be queued.
If the handler for the signal is established using a sigaction() call in which the SA_SIGINFO
flag is specified in the sa.sa_flags field, then a siginfo_t structure is passed as the second argument to the signal handler (Section 21.4). This structure contains fields identifying the file descriptor on which the event occurred, as well as the type of event.
si_fd: the file descriptor for which the I/O event occurred.
si_code: a code indicating the type of event that occurred. The values that can appear in this field, along with their general descriptions, are shown in Table 63-7.
si_band: a bit mask containing the same bits as are returned in the revents field by the poll() system call. The value set in si_code has a one-to-one correspondence with the bit-mask setting in si_band, as shown in Table 63-7.
We saw in Section 22.8 that there is a limit on the number of realtime signals that may be queued. If this limit is reached, the kernel reverts to delivering the default SIGIO
signal for “I/O possible” notifications. This informs the process that a signal-queue overflow occurred. When this happens, we lose information about which file descriptors have I/O events, because SIGIO
is not queued. (Furthermore, the SIGIO
handler doesn’t receive a siginfo_t argument, which means that the signal handler can’t determine the file descriptor that generated the signal.)
struct f_owner_ex { int type; pid_t pid; };
The type field defines the meaning of the pid field, and has one of the following values:
F_OWNER_PGRP
The pid field specifies the ID of a process group that is to be the target of “I/O possible” signals. Unlike with F_SETOWN
, a process group ID is specified as a positive value.
F_OWNER_PID
The pid field specifies the ID of a process that is to be the target of “I/O possible” signals.
F_OWNER_TID
The pid field specifies the ID of a thread that is to be the target of “I/O possible” signals. The ID specified in pid is a value returned by clone() or gettid().
The F_GETOWN_EX
operation is the converse of the F_GETOWN_EX
operation. It uses the f_owner_ex structure pointed to by the third argument of fcntl() to return the settings defined by a previous F_SETOWN_EX
operation.