简体   繁体   中英

Bad file descriptor in C with pipe(), dup2(), and fork()

I'm having trouble with file descriptors in C.

I have a primary method (called executeShell() ) that calls another method called getProgramParameters . getProgramParameters parses a few basic bash commands (eg "ls > output") and returns three things: the command ("ls" in the example), the input file if applicable (there isn't one in the example), and an output file if applicable ("output" in the example). It's a simplified redirection parser that also parses pipes, with the following exceptions: multiple pipes/redirections are not allowed, and it returns an error if you try to provide multiple inputs or outputs (eg "ls > output | cat" is invalid because of multiple outputs, and "ls | cat < output" is invalid because of multiple inputs). If a pipe is encountered, getProgramParameters is called again on the command after the pipe. The sets of parameters are called firstParams and secondParams , respectively.

In executeShell() , if I parse two sets of parameters (ie I encountered a pipe), I call pipe() once and fork() twice to make two children ( child_a and child_b respectively) that will be running the commands proper. I pipe the first program's ( child_a's ) write to the second program's ( child_b's ) read and close the unnecessary ends in each. I call another method called runCommand() in each child, which takes the values from first/secondParams and calls its own fork() to run the command (ls, cat, etc.) itself.

I explain all of this because somewhere in this process I'm getting bad file descriptors, and I can't tell why. The file descriptors for the input/output files of the actual commands (eg "cat < input > output") are generated by getProgramParameters() , and to complicate matters further I create stdin/out copies in both executeShell() and runCommand() so I can restore them after the children processes dup2() the input/output accordingly. I tried to output them to see what's going on, and I realized that there are file descriptors that are the same when they shouldn't be. I can't tell why this is happening.

The output for "ls > output" (no piping):

sh# ls > output
opened file for output with fd = 3
format: program > file
first params
  ls
  -1
  3
tokenizer null for firstParams, no second params
running program
stdinCopy: 3
stdoutCopy: 4
output fd = 3, duping that to STDOUT
Invalid: Error in duping output to STDOUT: Bad file descriptor
error in runCommand
sh#

and the output for "ls | cat > output":

sh# ls | cat > output
pipe found
first params
  ls
  -1
  -1
opened file for output with fd = 3
format: program > file
second params
  cat
  -1
  3
stdinCopy (child_b): 5
running second process
stdoutCopy (child_a): 5
running program
stdinCopy: 4
stdoutCopy: 6
output fd = 3, duping that to STDOUT
Invalid: Error in duping output to STDOUT: Bad file descriptor
Invalid: second program execution failed: Bad file descriptor
sh#

as a side note, don't worry about tokenizer, it's just a class that helps parse and indicates whether or not there's a pipe.

I apologize on the complexity of this and the methods in advance, I just don't understand why I'm getting the same file descriptor numbers, and all these methods are involved:

executeShell :

void executeShell() {
  char minishell[] = "sh# ";
  writeToStdout(minishell);

  TOKENIZER *tokenizer;
  tokenizer = getInput();
  if (tokenizer == NULL) {
    printf("%s\n", "Exiting...");
    exit(EXIT_SUCCESS);
  }
  if (*tokenizer->str == '\0') return;

  // when reading the first params,
  // no output is allowed if pipe
  PARAMS* firstParams;
  PARAMS* secondParams;
  firstParams = getProgramParameters(tokenizer);
  if (firstParams == NULL) {
    writeToStdout("first params null\n");
    return;
  }
  printf("%s\n", "first params");
  printf("  %s\n", firstParams->command);
  printf("  %d\n", firstParams->input);
  printf("  %d\n", firstParams->output);
  if (firstParams->tokenizer != NULL) {
    secondParams = getProgramParameters(firstParams->tokenizer);
    if (secondParams != NULL) {
      printf("%s\n", "second params");
      printf("  %s\n", secondParams->command);
      printf("  %d\n", secondParams->input);
      printf("  %d\n", secondParams->output);

      // at this point secondParams' tokenizer must be null because we only take one pipe
      if (secondParams->tokenizer != NULL) {
        printf("%s\n", "Invalid: multiple pipes");
        return;
      }
      if (secondParams->input != -1) {
        printf("%s\n", "Invalid: multiple inputs");
        return;
      }
      // create pipe, fork, etc.
      int fd[2], status;
      pid_t child_a, child_b;
      // char firstOutput[1024];

      if (pipe(fd) < 0) {
        printf("%s\n", "Invalid: pipe error");
        return;
      }
      if ((child_a = fork()) < 0) {
        printf("%s\n", "Invalid: fork error");
        return;
      } else if (child_a == 0) {
        // child_a
        // connecting write end (1) to stdout
        int stdoutCopy = dup(STDOUT_FILENO);
        printf("stdoutCopy (child_a): %d\n", stdoutCopy);
        if (dup2(fd[1], STDOUT_FILENO) == -1) {
          perror("Invalid: Error in duping output to pipe write");
          exit(EXIT_FAILURE);
        }
        // close read end (0)
        close(fd[0]);
        printf("%s\n", "running first process");
        if (runCommand(firstParams->command, firstParams->input, -1) != 0) {
          perror("Invalid: first program execution failed");
          dup2(stdoutCopy, STDOUT_FILENO);
          exit(EXIT_FAILURE);
        }
        if (dup2(STDOUT_FILENO, stdoutCopy) == -1) {
          perror("Invalid: Error in restoring STDOUT");
          exit(EXIT_FAILURE);
        }
        exit(EXIT_SUCCESS);
      } else {
        // parent
        child_b = fork();
        if (child_b < 0) {
          printf("%s\n", "Invalid: fork error");
          return;
        } else if (child_b == 0) {
          // child_b
          // connect read end (0) to stdin
          int stdinCopy = dup(STDIN_FILENO);
          printf("stdinCopy (child_b): %d\n", stdinCopy);
          if (dup2(fd[0], STDIN_FILENO) == -1) {
            perror("Invalid: Error in duping input to pipe read");
            exit(EXIT_FAILURE);
          }
          // close write end (1)
          close(fd[1]);
          printf("%s\n", "running second process");
          if (runCommand(secondParams->command, -1, secondParams->output) != 0) {
            perror("Invalid: second program execution failed");
            dup2(stdinCopy, STDIN_FILENO);
            exit(EXIT_FAILURE);
          }
          if (dup2(STDIN_FILENO, stdinCopy) == -1) {
            perror("Invalid: Error in restoring STDIN");
            exit(EXIT_FAILURE);
          }
          exit(EXIT_SUCCESS);
        } else {
          // back to parent
          do {
            if (wait(&status) == -1) {
              perror("Error in child process termination");
              exit(EXIT_FAILURE);
              return;
            }
          } while (!WIFEXITED(status) && !WIFSIGNALED(status));
          waitpid(child_a, &status, 0);
          waitpid(child_b, &status, 0);
        }
      }
      free_params(firstParams);
      free_params(secondParams);
      return;
    } else {
      printf("%s\n", "no second params");
    }
  } else {
    writeToStdout("tokenizer null for firstParams, no second params\n");
    if (runCommand(firstParams->command, firstParams->input, firstParams->output) != 0) {
      printf("%s\n", "error in runCommand");
    }
    free_params(firstParams);
    return;
  }
  // when reading the second params,
  // no input is allowed
  free_tokenizer(tokenizer);
  free_params(firstParams);
}

here's the bit from getProgramParameters that generates the file descriptor for "output" (where tok2 is just the second token such as "tok1 > tok2", and direction is the ">" or "<"):

if (strcmp(">", direction) == 0) {
    output = open(tok2, O_WRONLY | O_TRUNC | O_CREAT, 0644);
    if (output == -1) {
      printf("%s\n", "Invalid: second token file invalid");
      goto cleanup;
    } else {
      printf("%s%d\n", "opened file for output with fd = ", output);
    }

the relevant code from runCommand :

printf("%s\n", "running program");
  int status;
  childPid = fork();

  if (childPid < 0) {
    perror("Invalid: Error in creating child process");
    return -1;
  }

  if (childPid == 0) {
    // child
    // read in from file 
    // printf("%s\n", "test");
    int stdinCopy = dup(STDIN_FILENO);
    printf("stdinCopy: %d\n", stdinCopy);
    if (file != -1) {
      printf("%s\n", "file not null, duping file to STDIN");
      if (dup2(STDIN_FILENO, file) == -1) {
        perror("Invalid: Error in duping file to STDIN");
        return -1;
      }
    }
    // printf("%s\n", "test");
    int stdoutCopy = dup(STDOUT_FILENO);
    printf("stdoutCopy: %d\n", stdoutCopy);
    if (output != -1) {
      printf("%s%d%s\n", "output fd = ", output, ", duping that to STDOUT");
      if (dup2(STDOUT_FILENO, output) != 0) {
        perror("Invalid: Error in duping output to STDOUT");
        return -1;
      }
    }
    // printf("%s\n", "test");
    if (execvp(args[0], args) != 0) {
      // un dup
      if (dup2(stdinCopy, STDIN_FILENO) == -1 || dup2(stdoutCopy, STDOUT_FILENO) == -1) {
        perror("Invalid: Error in restoring STDIN/STDOUT");
        return -1;
      }
      close(stdinCopy);
      close(stdoutCopy);
      perror("Invalid: Error in execvp");
      // printf("%s\n", "test");
      output = -1;
      return -1;
    }
    // printf("%s\n", "test");
    if (dup2(stdinCopy, STDIN_FILENO) == -1 || dup2(stdoutCopy, STDOUT_FILENO) == -1) {
      perror("Invalid: Error in restoring STDIN/STDOUT");
      return -1;
    }
    close(stdinCopy);
    close(stdoutCopy);
    // printf("%s\n", "process run succesfully");
  } else {
    // parent
    do {
      if (wait(&status) == -1) {
        perror("Error in child process termination");
        exit(EXIT_FAILURE);
        return -1;
      }
    } while (!WIFEXITED(status) && !WIFSIGNALED(status));
    // reset childPid
    childPid = 0;
  }
  for (int i=0; i<numArgs; i++) {
    free(args[i]);
  }
  return 0;
}

I'm also getting a zombie process dependent on the command, and I'm not sure why that's happening, either. I tried to call wait() appropriately and ensure each process exited regardless...

Update: I have commented out the stdin/out copies, as I suspected they may be unnecessary. I'm still getting the file descriptor errors, though. I also tried to provide a copy of the stdin/out to the runCommand() method suspecting that I was closing the standard versions as a commenter pointed out, this doesn't work either.

Second update: Changed all "file" variables names to "input" for consistency, called correct _exit in child in runCommand() , no longer getting zombies. Tried changing my PARAMS class to take input and output as integer pointers instead (thinking that maybe there was something special about the integer returned from an open call for the file descriptors), but it turns out that children from fork really don't like the pointer within the class. I was getting this (changed some prints too):

sh# ls > output
initializing input as -1
initializing output as 3
runCommand seeing input as -1 and output as 3
input fd is 1484746160, duping that to STDIN (0)
Invalid: Error in duping input to STDIN
Bad file descriptor
sh# Exiting...

In sum, still getting "bad file descriptor" even if I'm only calling dup2(output, STDOUT_FILENO) (not shown above).

Update: I just realized that my dup2 check should see if it's -1, not anything but 0. dup2 will return the new file descriptor.

I fixed the problem by calling open() within the child itself. I could not for the life of me figure out why the file descriptor wasn't working as a straight integer - maybe once it exited the method or forked it tried to free up resources or something? Idk, but it forced me to restructure a lot, but it worked in the end.

Your approach has some principal flaws.

First, in child (when you detected childpid == 0) when execvp() fails, you shall NOT return because this continues execution in child of parentʼs code before fork(). You should explicitly finish the child using _exit (notice: even not exit because the latter will try to flush stdio buffers and do other closings). As the convention, call _exit(127) in such case. More so, perror() isnʼt permitted in child. Pass the error code in another way to parent and print it there. (But you can, yes, use open , write , close that doesn't affect libc state.)

Second, file for input descriptor but output for output one - this really confuses. Letʼs make all them consistent.

Third, duplication of pipe ends to STDIN_FILENO or STDOUT_FILENO (what of them is needed at what pipe end) shall be done before exec*() . If exec*() succeeds, the following code isnʼt executed at all.

Seems you try to reach this but dup2(STDOUT_FILENO, output) is wrong: you need the opposite direction: STDOUT_FILENO shall be the target descriptor, not the source one. This code just does not any redirection.

Fourth, you either donʼt need cycle around wait() in parent, or you should check errno after its failure to EINTR and restart on this case.

Thatʼs all on a quick glance but is enough to retry after fixing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM