Marco Merlini
Marco Merlini

Reputation: 965

Linux timeout for read() on named pipe

Suppose I create a named pipe on a Linux system:

$ mkfifo my_pipe

The next thing I want to do is write a little monitor program which tries to read() from my_pipe, but times out after a while. In the following pseudo-code, I have used a fictional function wait_for_avail(fd, timeout_ms):

int fd = open("my_pipe", O_RDONLY);
while (1) {
    //Fictional wait_for_avail(fd, timeout_ms). Is there a real function
    //that has this behaviour?
    int rc = wait_for_avail(fd, 500);
    if (rc == 1) {
        char buf[64];
        read(fd, buf, 64);
        //do something with buf
    } else {
        fprintf(stderr, "Timed out while reading from my_pipe\n");
        //do something else in the program
    }
}

I thought poll with the POLLIN flag might work, but it does not. From my simple trials, I have found that it simply waits until another process has opened the named pipe for writing (but not for data to be available, i.e. read() would not block). By the way, for some reason, poll ignores your timeout and just seems to block forever until another process opens the pipe.

The only other solution I can think of is to open() the file with O_NONBLOCK, and sort of manually watch the time going by as I constantly try read()ing with a count of 0 bytes.

Is there a better solution out there?

EDIT: The process I have here blocks on opening the named pipe. However, if you use the O_NONBLOCK flag, the file opens right away. At that point, poll() can be used to wait (with an optional timeout) for the other end of the pipe to be opened for writing.

However, this still does have the behaviour of implementing a timeout for the read() function. It still appears to block as soon as you call read() (even if the pipe was opened with O_NONBLOCK)

Upvotes: 5

Views: 5237

Answers (2)

Marco Merlini
Marco Merlini

Reputation: 965

After a lot of help and patience from @Shawn, I managed to come up with an answer I found satisfying. Here are the contents of a file called pipe_watcher.c:

#include <stdio.h>  //printf etc.
#include <errno.h>  //errno
#include <string.h> //perror
#include <signal.h> //SIGALRM, sigaction, sigset
#include <time.h>   //timer_create, timer_settime
#include <fcntl.h>  //open, O_RDONLY
#include <unistd.h> //close

/* This code demonstrates how you can monitor a named pipe with timeouts on the
 * read() system call.
 * 
 * Compile with:
 * 
 *  gcc -o pipe_watcher pipe_watcher.c -lrt
 * 
 * And run with:
 * 
 *  ./pipe_watcher PIPE_FILENAME
*/

//Just needed a dummy handler
void sigalrm_handler(int s) {
    return;
}

int main(int argc, char **argv) {
    //Check input argument count
    if (argc != 2) {
        puts("Usage:\n");
        puts("\t./pipe_watcher PIPE_FILENAME");
        return -1;
    }

    //Create a timer object
    timer_t clk;
    int rc = timer_create(CLOCK_REALTIME, NULL, &clk);
    if (rc < 0) {
        perror("Could not create CLOCK_REALTIME timer");
        return -1;
    }

    //Create some time values for use with timer_settime
    struct itimerspec half_second = {
        .it_interval = {.tv_sec = 0, .tv_nsec = 0},
        .it_value = {.tv_sec = 0, .tv_nsec = 500000000}
    };

    struct itimerspec stop_timer = {
        .it_interval = {.tv_sec = 0, .tv_nsec = 0},
        .it_value = {.tv_sec = 0, .tv_nsec = 0}
    };

    //Set up SIGALRM handler
    struct sigaction sigalrm_act = {
        .sa_handler = sigalrm_handler,
        .sa_flags = 0
    };
    sigemptyset(&sigalrm_act.sa_mask);
    rc = sigaction(SIGALRM, &sigalrm_act, NULL);
    if (rc < 0) {
        perror("Could not register signal handler");
        timer_delete(clk);
        return -1;
    }

    //We deliberately omit O_NONBLOCK, since we want blocking behaviour on
    //read(), and we're willing to tolerate dealing with the blocking open()
    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        char msg[80];
        sprintf(msg, "Could not open [%s]", argv[1]);
        perror(msg);
        timer_delete(clk);
        return -1;
    }

    puts("File opened");

    while (1) {
        //Buffer to read() into
        char buf[80];
        int len;

        //Set up a timer to interrupt the read() call after 0.5 seconds
        timer_settime(clk, 0, &half_second, NULL);

        //Issue read() system call
        len = read(fd, buf, 80);

        //Check for errors. The else-if checks for EOF
        if (len < 0) {
            if (errno == EINTR) {
                //This means we got interrupted by the timer; we can keep going
                fprintf(stderr, "Timeout, trying again\n");
                continue;
            } else {     
                //Something really bad happened. Time to quit.       
                perror("read() failed");
                //No point waiting for the timer anymore
                timer_settime(clk, 0, &stop_timer, NULL);
                break;
            }
        } else if (len == 0) {
            puts("Reached end of file");
            break;
        }

        //No error or EOF; stop the timer and print the results
        timer_settime(clk, 0, &stop_timer, NULL);
        write(STDOUT_FILENO, buf, len);
    }

    //Cleanup after ourselves
    timer_delete(clk);
    close(fd);
    return 0;
}

The technique is to set up a timer before a (blocking) read() call. Then, we can simply check the return value of read() to see if it was interrupted due to a timeout, if a general error occurred, if EOF was reached, or if it successfully read data.

There's only one snag: you can't open the file in non-blocking mode; this causes open() to block until another process opens the pipe for writing. However, in my application this is actually a desirable feature. You could also set up SIGALRM to enforce a timeout on the open(), or maybe do it in another thread.

In fact, this technique should work with any other system call, so I might put together a little helper library to make this pattern easier to use.

EDIT

One more thing: it is very important to not use the SA_RESTART flag when registering the signal handler. Otherwise, even if a system call is interrupted by a signal, Linux will try it again after the signal is handled.

Upvotes: 0

Shawn
Shawn

Reputation: 52674

Your idea about opening the fifo in non-blocking mode is correct. If you do that, poll()/select()/etc. can be used to wait for the other end to be opened, or timeout first.

The following example program just runs in an infinite loop waiting for other programs to write to my_pipe and echos the written text, with the occasional status update when there's no data or writer:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  while (1) {
    int fd = open("my_pipe", O_RDONLY | O_NONBLOCK);
    if (fd < 0) {
      perror("open");
      return EXIT_FAILURE;
    }

    struct pollfd waiter = {.fd = fd, .events = POLLIN};

    while (1) {
      // 10 second timeout
      switch (poll(&waiter, 1, 10 * 1000)) {
      case 0:
        puts("The fifo timed out.");
        break;
      case 1:
        if (waiter.revents & POLLIN) {
          char buffer[BUFSIZ];
          ssize_t len = read(fd, buffer, sizeof buffer - 1);
          if (len < 0) {
            perror("read");
            return EXIT_FAILURE;
          }
          buffer[len] = '\0';
          printf("Read: %s\n", buffer);
        } else if (waiter.revents & POLLERR) {
          puts("Got a POLLERR");
          return EXIT_FAILURE;
        } else if (waiter.revents & POLLHUP) {
          // Writer closed its end
          goto closed;
        }
        break;
      default:
        perror("poll");
        return EXIT_FAILURE;
      }
    }
  closed:
    if (close(fd) < 0) {
      perror("close");
      return EXIT_FAILURE;
    }
  }
}

Upvotes: 3

Related Questions