Previous Section Next Section

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:

  1. A signal handler must be established for the SIGIO signal.

  2. The socket owner must be set, normally with the F_SETOWN command of fcntl (Figure 7.20).

  3. 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

  • A datagram arrives for the socket

  • An asynchronous error occurs on the socket

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.

Figure 25.1. Two different ways to build a UDP server.

graphics/25fig01.gif


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.

Previous Section Next Section