I/O With Timeout

From CodeCodex

Revision as of 03:24, 20 September 2007 by Nostromo (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

When doing I/O on a network connection, or when reading input from a user, it’s often useful to be able to impose a maximum interval that you’re willing to wait for input. This can be done quite easily with the select(2) system call.

Assume the connection or console is open on a file descriptor called TheConn. Supposing the timeout is ConnTimeout seconds. Then the following sequence will implement a read timeout:

fd_set InputReady, OutputReady, Problems;
struct timeval WaitTimeStruct;
...
FD_ZERO(&InputReady);
FD_SET(TheConn, &InputReady);
FD_ZERO(&OutputReady);
FD_ZERO(&Problems);
FD_SET(TheConn, &Problems);
WaitTimeStruct.tv_sec = ConnTimeout;
WaitTimeStruct.tv_usec = 0;
if
  (
        select
          (
            /*nfds =*/ TheConn + 1,
            /*readfds =*/ &InputReady,
            /*writefds =*/ &OutputReady,
            /*exceptfds =*/ &Problems,
            /*timeout =*/ &WaitTimeStruct
          )
    <
        0
  )
    ... abort due to error ...
if (FD_ISSET(TheConn, &Problems))
  {
    errno = EREMOTEIO; /* or something*/
    ... abort due to connection problem ...
  }
else if (!FD_ISSET(TheConn, &InputReady))
  {
    errno = ETIMEDOUT;
    ... abort due to no input ready ...
  } /*if*/
... OK, input ready, do actual read from TheConn here ...

Output is similar, except that the OutputReady bitmask should be set and tested instead of InputReady.

Note that if you’re reading more than one character, then it is still possible to block indefinitely if fewer characters were received than you were expecting. To fix this, TheConn needs to be set for non-blocking I/O, as follows:

fcntl(TheConn, F_SETFL, O_NONBLOCK);

Now, the read-with-timeout needs to be done in a loop. Suppose the input buffer is pointed to by unsigned char * Buffer, and the number of input bytes expected is size_t BytesExpected. Then the read-with-timeout sequence becomes:

errno = 0; /* to begin with */
for (;;)
  {
    int ReadResult;
    if (BytesExpected == 0)
        break; /* read all done */
    FD_ZERO(&InputReady);
    FD_SET(TheConn, &InputReady);
    FD_ZERO(&OutputReady);
    FD_ZERO(&Problems);
    FD_SET(TheConn, &Problems);
    WaitTimeStruct.tv_sec = ConnTimeout;
    WaitTimeStruct.tv_usec = 0;
    if
      (
            select
              (
                /*nfds =*/ TheConn + 1,
                /*readfds =*/ &InputReady,
                /*writefds =*/ &OutputReady,
                /*exceptfds =*/ &Problems,
                /*timeout =*/ &WaitTimeStruct
              )
        <
            0
      )
        break; /* errno will be set */
    if (FD_ISSET(TheConn, &Problems))
      {
        errno = EREMOTEIO; /* or something*/
        break;
      }
    else if (!FD_ISSET(TheConn, &InputReady))
      {
        errno = ETIMEDOUT;
        break;
      } /*if*/
    ReadResult = read(TheConn, Buffer, BytesExpected);
    if (ReadResult <= 0)
        break; /* EOF or unexpected error (won't be EAGAIN) */
    Buffer += ReadResult;
    BytesExpected -= ReadResult;
  } /*for*/

On completion of the above loop, if errno is nonzero, then an error occurred. Otherwise, if BytesExpected is nonzero, then EOF was encountered (or the connection was closed) before the required number of bytes was read. Otherwise, the read completed successfully.