Search code examples
cpipefile-descriptorposix-select

select returns EBADF when using stdin and pipes in the fdset


In this program I am attempting to set up some pipes to dup2 a child process's "stdin, stdout, stderr" to instead send and receive that data through pipes managed by the parent process; however, something seems to be wrong with my setup as select is returning -1 with errno 9 (EBADF, correct?). I'm not sure how this could be the case unless somehow I'm closing too many / not closing the correct fds on the child or parent end. Does it have something to do with me putting my pipes in a global scope?

/* GLOBALS */
int mode; // i for input mode, c for command mode, i/o for input/output mode
int cpid; // child's process id
int fderr[2]; // stderr pipe
int fdsin[2]; // stdin pipe
int fdout[2]; // stdout pipe

int main(int argc, char *argv[]){
    
    // pipe 3
    pipe(fderr);
    pipe(fdsin);
    pipe(fdout);
    
    // fork
    if ((cpid = fork()) == 0) {
        // dupe 3 for the child
        dup2(fderr[1], 2);
        dup2(fdout[1], 1);
        dup2(fdsin[0], 0);
        
        // close the 6 pipes
        close(fderr[0]);
        close(fderr[1]);
        close(fdout[0]);
        close(fdout[1]);
        close(fdsin[0]);
        close(fdsin[1]);
        
        // execute
        execvp(argv[1], argv + 1);
        exit(0);
    }
    else {
        // close unused pipes
        close(fderr[1]);
        close(fdout[1]);
        close(fdsin[0]);

        // setup fdset and mode
        mode = 'c';
        fd_set set;
        
        // setup capturing of child process termination
        int code;
        int status;

        // handle pipe reads and writes
        while (1) {
            switch ( mode ) {
                // clear previous fdset
                FD_ZERO(&set);
                
                // always add stderr
                FD_SET(fderr[0], &set);
                
                // input and command only care about stdin
                case 'i':
                case 'c':
                    FD_SET(0, &set);
                    break;
                
                // input / output cares about stdin and stdout
                case 'o':
                    FD_SET(fdout[0], &set);
                    FD_SET(0, &set);
                    break;
            }
            
            // select
            if (select(FD_SETSIZE, &set, NULL, NULL, NULL) < 0)
            {
                int err = errno;
                fprintf(stderr, "%d\n", err);
                exit(EXIT_FAILURE);
            }
    // Rest of code currently unreachable...


Solution

  • The switch statement in the original code called FD_ZERO and FD_SET in a place that is unreachable. The call to FD_ZERO and FD_SET should be placed before the switch statement.

    // clear previous fdset
    FD_ZERO(&set);
    
    // always add stderr
    FD_SET(fderr[0], &set);
    
    switch ( mode ) {
        
        // input and command only care about stdin
        case 'i':
        case 'c':
            FD_SET(0, &set);
            break;
        
        // input / output cares about stdin and stdout
        case 'o':
            FD_SET(fdout[0], &set);
            FD_SET(0, &set);
            break;
    }