Search code examples
cprocesspipeforkposix

pselect notifies both pipe read end file descriptors even though only one write end was written on


I fail to understand the behavior of pselect. Basically what I do is the following:

  • Register a handler for the SIGCHILD signal
  • Create two pipes
  • Create a child process using fork
  • Sleep in the child for 5 seconds, then exit
  • In the parent process call pselect, waiting on the read ends of the two pipes
  • When the child process terminates write something in the first pipe from inside the SIGCHILD handler.
  • pselect returns in the parent process with both file descriptors set

I expect the output of the following code to be:

Pipe1 is set!

But, instead I get:

Pipe1 is set!
Pipe2 is set!

Why are both pipe read end file descriptors set when I only write in one pipe write end? Is this behavior part of normal pselect spurious file descriptor notifications? What am I doing wrong?

Here's the program:

    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include<iostream>

    enum PipeEnd {
        READ_END = 0, WRITE_END = 1, MAX_END
    };

    int pipe1[MAX_END], pipe2[MAX_END];

    void handle_sigchld(const int signal) {
        //In the SIGCHLD handler write the process ID on pipe1
        int returnStatus;
        int childPID = waitpid(static_cast<pid_t>(-1), &returnStatus, WNOHANG);
        write(pipe1[WRITE_END], &childPID, sizeof(childPID));
    }

    void createPipe(int newPipe[MAX_END]) {
        pipe(newPipe);
        fcntl(newPipe[READ_END], F_SETFL, O_NONBLOCK);
        fcntl(newPipe[WRITE_END], F_SETFL, O_NONBLOCK);
    }

    int main(int argc, const char** argv) {
        //Add a handler for the SIGCHLD signal
        struct sigaction sa;
        sa.sa_handler = &handle_sigchld;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
        sigaction(SIGCHLD, &sa, nullptr);

        //Create two pipes
        createPipe(pipe1);
        createPipe(pipe2);

        //Create a child process
        if (0 == fork()) {
            sleep(5);
            exit(0);
        }

        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(pipe1[READ_END], &read_fds);
        FD_SET(pipe2[READ_END], &read_fds);
        int maxfd = std::max(pipe1[READ_END], pipe2[READ_END]);
        //Wait for a file descriptor to be notified   
        pselect(maxfd + 1, &read_fds, nullptr, nullptr, nullptr, nullptr);

        //Check if the read ends of the two pipes are set/notified
        if (FD_ISSET(pipe1[READ_END], &read_fds))
            std::cout << "Pipe1 is set!" << std::endl;
        if (FD_ISSET(pipe2[READ_END], &read_fds))
            std::cout << "Pipe2 is set!" << std::endl;
        return 0;
    }

Solution

  • You be surprised that the the program exhibits the same behaviour even if the signal handler doesn't write anything.

    The reason is that pselect fails. Quoting man 7 signal,

    The following interfaces are never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR when interrupted by a signal handler:

    ....

    • File descriptor multiplexing interfaces: epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), and pselect(2).

    Always test what the system call returns.