14.2 Socket Timeouts
There are three ways to place a timeout on an
I/O operation involving a socket:
-
Call
alarm, which generates the SIGALRM signal when
the specified time has expired. This involves signal handling,
which can differ from one implementation to the next, and it may
interfere with other existing calls to alarm in the
process.
-
Block waiting
for I/O in select, which has a time limit built-in,
instead of blocking in a call to read or
write.
-
Use the newer
SO_RCVTIMEO and SO_SNDTIMEO socket options. The
problem with this approach is that not all implementations support
these two socket options.
All three techniques work with input and output
operations (e.g., read, write, and other
variations such as recvfrom and sendto), but we
would also like a technique that we can use with connect,
since a TCP connect can take a long time to time out (typically 75
seconds). select can be used to place a timeout on
connect only when the socket is in a nonblocking mode
(which we show in Section 16.3), and
the two socket options do not work with connect. We also
note that the first two techniques work with any descriptor, while
the third technique works only with socket descriptors.
We now show examples of all three
techniques.
connect with a Timeout Using
SIGALRM
Figure
14.1 shows our function connect_timeo that calls
connect with an upper limit specified by the caller. The
first three arguments are the three required by connect
and the fourth argument is the number of seconds to wait.
Figure 14.1
connect with a timeout.
lib/connect_timeo.c
1 #include "unp.h"
2 static void connect_alarm(int);
3 int
4 connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
5 {
6 Sigfunc *sigfunc;
7 int n;
8 sigfunc = Signal(SIGALRM, connect_alarm);
9 if (alarm(nsec) != 0)
10 err_msg("connect_timeo: alarm was already set");
11 if ( (n = connect(sockfd, saptr, salen)) < 0) {
12 close(sockfd);
13 if(errno == EINTR)
14 errno = ETIMEDOUT;
15 }
16 alarm(0); /* turn off the alarm */
17 Signal(SIGALRM, sigfunc); /* restore previous signal handler */
18 return (n);
19 }
20 static void
21 connect_alarm(int signo)
22 {
23 return; /* just interrupt the connect() */
24 }
Establish signal handler
8 A
signal handler is established for SIGALRM. The current
signal handler (if any) is saved, so we can restore it at the end
of the function.
Set alarm
9鈥?0
The alarm clock for the process is set to the number of seconds
specified by the caller. The return value from alarm is
the number of seconds currently remaining in the alarm clock for
the process (if one has already been set by the process) or 0 (if
there is no current alarm). In the former case we print a warning
message since we are wiping out that previously set alarm (see
Exercise
14.2).
Call connect
11鈥?5
connect is called and if the function is interrupted
(EINTR), we set the errno value to
ETIMEDOUT instead. The socket is closed to prevent the
three-way handshake from continuing.
Turn off alarm and restore any
previous signal handler
16鈥?8
The alarm is turned off by setting it to 0 and the previous signal
handler (if any) is restored.
Handle SIGALRM
20鈥?4
The signal handler just returns, assuming this return will
interrupt the pending connect, causing connect to
return an error of EINTR. Recall our signal
function (Figure 5.6) that does
not set the SA_RESTART flag when the signal being caught
is SIGALRM.
One point to make with this example is that we
can always reduce the timeout period for a connect using
this technique, but we cannot extend the kernel's existing timeout.
That is, on a Berkeley-derived kernel the timeout for a
connect is normally 75 seconds. We can specify a smaller
value for our function, say 10, but if we specify a larger value,
say 80, the connect itself will still time out after 75
seconds.
Another point with this example is that we use
the interruptibility of the system call (connect) to
return before the kernel's time limit expires. This is fine when we
perform the system call and can handle the EINTR error
return. But in Section 29.7, we
will encounter a library function that performs the system call,
and the library function reissues the system call when
EINTR is returned. We can still use SIGALRM in
this scenario, but we will see in Figure 29.10 that we
also have to use sigsetjmp and siglongjmp to get
around the library's ignoring of EINTR.
Although this example is fairly simple, signals
are quite difficult to use correctly with multithreaded programs
(see Chapter
26). So, the technique shown here is only recommended for
single-threaded programs.
recvfrom with a Timeout Using
SIGALRM
Figure
14.2 is a redo of our dg_cli function from Figure
8.8, but with a call to alarm to interrupt the
recvfrom if a reply is not received within five
seconds.
Figure 14.2
dg_cli function with alarm to timeout
recvfrom.
advio/dgclitimeo3.c
1 #include "unp.h"
2 static void sig_alrm(int);
3 void
4 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
5 {
6 int n;
7 char sendline[MAXLINE], recvline[MAXLINE + 1];
8 Signal(SIGALRM, sig_alrm);
9 while (Fgets(sendline, MAXLINE, fp) != NULL) {
10 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
11 alarm(5);
12 if ( (n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0) {
13 if (errno == EINTR)
14 fprintf(stderr, "socket timeout\n");
15 else
16 err_sys("recvfrom error");
17 } else {
18 alarm(0);
19 recvline[n] = 0; /* null terminate */
20 Fputs(recvline, stdout);
21 }
22 }
23 }
24 static void
25 sig_alrm(int signo)
26 {
27 return; /* just interrupt the recvfrom() */
28 }
Handle timeout from
recvfrom
8鈥?2
We establish a signal handler for SIGALRM and then call
alarm for a five-second timeout before each call to
recvfrom. If recvfrom is interrupted by our
signal handler, we print a message and continue. If a line is read
from the server, we turn off the pending alarm and print
the reply.
SIGALRM signal handler
24鈥?8
Our signal handler just returns, to interrupt the blocked
recvfrom.
This example works correctly because we are
reading only one reply each time we establish an alarm. In
Section 20.4, we
will use the same technique, but since we are reading multiple
replies for a given alarm, a race condition exists that we
must handle.
recvfrom with a Timeout Using
select
We demonstrate the second technique for setting
a timeout (using select) in Figure 14.3. It shows our function named
readable_timeo which waits up to a specified number of
seconds for a descriptor to become readable.
Figure 14.3
readable_timeo function: waits for a descriptor to become
readable.
lib/readable_timeo.c
1 #include "unp.h"
2 int
3 readable_timeo(int fd, int sec)
4 {
5 fd_set rset;
6 struct timeval tv;
7 FD_ZERO(&rset);
8 FD_SET(fd, &rset);
9 tv.tv_sec = sec;
10 tv.tv_usec = 0;
11 return (select(fd + 1, &rset, NULL, NULL, &tv));
12 /* > 0 if descriptor is readable */
13 }
Prepare arguments for
select
7鈥?0
The bit corresponding to the descriptor is turned on in the read
descriptor set. A timeval structure is set to the number
of seconds that the caller wants to wait.
Block in select
11鈥?2
select waits for the descriptor to become readable, or for
the timeout to expire. The return value of this function is the
return value of select: 鈥? on an error, 0 if a timeout
occurs, or a positive value specifying the number of ready
descriptors.
This function does not perform the read
operation; it just waits for the descriptor to be ready for
reading. Therefore, this function can be used with any type of
socket, TCP or UDP.
It is trivial to create a similar function named
writable_timeo that waits for a descriptor to become
writable.
We use this function in Figure 14.4, which is a redo of our
dg_cli function from Figure 8.8. This new
version calls recvfrom only when our
readable_timeo function returns a positive value.
We do not call recvfrom until the
function readable_timeo tells us that the descriptor is
readable. This guarantees that recvfrom will not
block.
Figure 14.4
dg_cli function that calls readable_timeo to set
a timeout.
advio/dgclitimeo1.c
1 #include "unp.h"
2 void
3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
4 {
5 int n;
6 char sendline[MAXLINE], recvline[MAXLINE + 1];
7 while (Fgets(sendline, MAXLINE, fp) != NULL) {
8 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
9 if (Readable_timeo(sockfd, 5) == 0) {
10 fprintf(stderr, "socket timeout\n");
11 } else {
12 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
13 recvline[n] = 0; /* null terminate */
14 Fputs(recvline, stdout);
15 }
16 }
17 }
recvfrom with a Timeout Using
the SO_RCVTIMEO Socket Option
Our final example demonstrates the
SO_RCVTIMEO socket option. We set this option once for a
descriptor, specifying the timeout value, and this timeout then
applies to all read operations on that descriptor. The nice thing
about this method is that we set the option only once, compared to
the previous two methods, which required doing something before
every operation on which we wanted to place a time limit. But this
socket option applies only to read operations, and the similar
option SO_SNDTIMEO applies only to write operations;
neither socket option can be used to set a timeout for a
connect.
Figure
14.5 shows another version of our dg_cli function that
uses the SO_RCVTIMEO socket option.
Set socket option
8鈥?0
The fourth argument to setsockopt is a pointer to a
timeval structure that is filled in with the desired
timeout.
Test for timeout
15鈥?7
If the I/O operation times out, the function (recvfrom, in
this case) returns EWOULDBLOCK.
Figure 14.5
dg_cli function that uses the SO_RCVTIMEO socket
option to set a timeout.
advio/dgclitimeo2.c
1 #include "unp.h"
2 void
3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
4 {
5 int n;
6 char sendline[MAXLINE], recvline[MAXLINE + 1];
7 struct timeval tv;
8 tv.tv_sec = 5;
9 tv.tv_usec = 0;
10 Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
11 while (Fgets(sendline, MAXLINE, fp) != NULL) {
12 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
13 n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
14 if (n < 0) {
15 if (errno == EWOULDBLOCK) {
16 fprintf(stderr, "socket timeout\n");
17 continue;
18 } else
19 err_sys("recvfrom error");
20 }
21 recvline[n] = 0; /* null terminate */
22 Fputs(recvline, stdout);
23 }
24 }
|