25.2 Signal-Driven I/O for
Sockets
To use signal-driven I/O with a socket
(SIGIO) requires the process to perform the following
three steps:
-
A signal handler
must be established for the SIGIO signal.
-
The
socket owner must be set, normally with the F_SETOWN
command of fcntl (Figure 7.20).
-
Signal-driven I/O must be enabled for the socket, normally with
the F_SETFL command of fcntl to turn on the
O_ASYNC flag (Figure 7.20).
The O_ASYNC flag is a relatively late
addition to the POSIX specification. Very few systems have
implemented support for the flag. In Figure 25.4, we will
enable signal-driven I/O with the FIOASYNC ioctl instead.
Notice the bad choice of names by POSIX: The name O_SIGIO
would have been a better choice for the new flag.
We should establish the signal handler
before setting the owner of the
socket. Under Berkeley-derived implementations, the order of the
two function calls does not matter because the default action is to
ignore SIGIO. Therefore, if we were to reverse the order
of the two function calls, there is a small chance that a signal
could be generated after the call to fcntl but before the
call to signal; if that happens, the signal is just
discarded. Under SVR4, however, SIGIO is defined to be
SIGPOLL in the <sys/signal.h> header and
the default action of SIGPOLL is to terminate the process.
Therefore, under SVR4, we want to be certain the signal handler is
installed before setting the owner of the socket.
Although setting a socket for signal-driven I/O
is easy, the hard part is determining what conditions cause
SIGIO to be generated for the socket owner. This depends
on the underlying protocol.
SIGIO with UDP Sockets
Using signal-driven I/O with UDP is simple. The
signal is generated whenever
Hence, when we catch SIGIO for a UDP
socket, we call recvfrom to either read the datagram that
arrived or to obtain the asynchronous error. We talked about
asynchronous errors with regard to UDP sockets in Section
8.9. Recall that these are generated only if the UDP socket is
connected.
SIGIO is generated for these two
conditions by the calls to sorwakeup on pp. 775, 779, and
784 of TCPv2.
SIGIO with TCP Sockets
Unfortunately, signal-driven I/O is next to
useless with a TCP socket. The problem is that the signal is
generated too often, and the occurrence of the signal does not tell
us what happened. As noted on p. 439 of TCPv2, the following
conditions all cause SIGIO to be generated for a TCP
socket (assuming signal-driven I/O is enabled):
-
A connection request has completed on a
listening socket
-
A disconnect request has been initiated
-
A disconnect request has completed
-
Half of a connection has been shut down
-
Data has arrived on a socket
-
Data has been sent from a socket (i.e., the
output buffer has free space)
-
An asynchronous error occurred
For example, if one is both reading from and
writing to a TCP socket, SIGIO is generated when new data
arrives and when data previously written is acknowledged, and there
is no way to distinguish between the two in the signal handler. If
SIGIO is used in this scenario, the TCP socket should be
set to nonblocking to prevent a read or write
from blocking. We should consider using SIGIO only with a
listening TCP socket, because the only condition that generates
SIGIO for a listening socket is the completion of a new
connection.
The only real-world use of signal-driven I/O
with sockets that the authors were able to find is the NTP server,
which uses UDP. The main loop of the server receives a datagram
from a client and sends a response. But, there is a non-negligible
amount of processing to do for each client's request (more than our
trivial echo server). It is important for the server to record
accurate timestamps for each received datagram, since that value is
returned to the client and then used by the client to calculate the
RTT to the server. Figure
25.1 shows two ways to build such a UDP server.
Most UDP servers (including our echo server from
Chapter 8)
are designed as shown at the left of this figure. But the NTP
server uses the technique shown on the right side: When a new
datagram arrives, it is read by the SIGIO handler, which
also records the time at which the datagram arrived. The datagram
is then placed on another queue within the process from which it
will be removed by and processed by the main server loop. Although
this complicates the server code, it provides accurate timestamps
of arriving datagrams.
Recall from Figure 22.4 that the
process can set the IP_RECVDSTADDR socket option to
receive the destination address of a received UDP datagram. One
could argue that two additional pieces of information that should
also be returned for a received UDP datagram are an indication of
the received interface (which can differ from the destination
address, if the host employs the common weak end system model) and
the time at which the datagram arrived.
For IPv6, the IPV6_PKTINFO socket
option (Section 22.8)
returns the received interface. For IPv4, we discussed the
IP_RECVIF socket option in Section 22.2.
FreeBSD also provides the SO_TIMESTAMP
socket option, which returns the time at which the datagram was
received as ancillary data in a timeval structure. Linux
provides an SIOCGSTAMP ioctl that returns a
timeval structure containing the time at which the
datagram was received.
|