C Shell 处理管道时挂起

C Shell hanging when dealing with piping

我正在开发 C shell,但在使用任意数量的管道时遇到了问题。当我 运行 shell 时,它挂在任何管道上。出于某种原因,当我执行 ls -la | sort 时,它会一直挂起,直到我输入内容并按下 Ctrl+D。我知道这与未关闭的管道有关,但打印语句显示管道 3、4、5 在父项和子项中都已关闭。我已经做了几个小时了,不知道为什么这不起作用。任何帮助将不胜感激。

原码:

char *current_command;
current_command = strtok_r(cmdline_copy, "|", &cmdline_copy);
char *commands[100][MAX_ARGS]; //Max 100 piped commands with each having MAX_ARGS arguments
int i = 0;
while (current_command != NULL) { //Go through each command and add it to the array
    char *copy = malloc(strlen(current_command)*sizeof(char)); //Copy of curretn command
    strcpy(copy, current_command);
    char *args_t[MAX_ARGS];
    int nargs_t = get_args(copy, args_t);
    memcpy(commands[i], args_t, sizeof(args_t)*nargs_t); //Copy the command and it's arguments to the 2d array
    i++;
    current_command = strtok_r(NULL, "|\n", &cmdline_copy); //Use reentrant version of strtok to prevent fighting with get_args function
}
int fd[2*(i-1)]; //Set up the pipes i.e fd[0,1] is first pipe, fd[1,2] second pipe, etc.
for (int j = 0; j < i*2; j+=2) {
    pipe(fd+j);
}
//Here is where we do the commands
for (int j = 0; j < i; j++) {
    pid = fork(); //Fork
    if (pid == 0) { //Child process
        if (j == 0) { //First process
            printf("Child Closed %d\n", fd[0]);
            close(fd[0]);
            dup2(fd[1], fileno(stdout));
        }
        else if (j == i -1) { //Last process
            dup2(fd[j], fileno(stdin));
            printf("Child closed %d\n", fd[j]);
            printf("Child closed %d\n", fd[j+1]);
            close(fd[j+1]);
            close(fd[j]);
        }
        else { //Middle processes
            dup2(fd[j], fileno(stdin));
            dup2(fd[j+1], fileno(stdout));
            printf("Child closed %d\n", fd[j]);
            close(fd[j]);
        }
        execvp(commands[j][0], commands[j]);
    }
    else if (pid > 0) { //Parent
        printf("Parent closed %d\n", fd[j]);
        close(fd[j]);
        printf("Parent closed %d\n", fd[j+1]);
        close(fd[j+1]);
        waitpid(pid, NULL, 0); //Wait for the process
    }
    else {
        perror("Error with fork");
        exit(1);
    }
}

最终代码:

char *current_command;
current_command = strtok_r(cmdline_copy, "|", &cmdline_copy);
char *commands[100][MAX_ARGS]; //Max 100 piped commands with each having MAX_ARGS arguments
int command_count = 0;
while (current_command != NULL) { //Go through each command and add it to the array
   char *copy = malloc(strlen(current_command)*sizeof(char)); //Copy of curretn command because get_args uses strtok
   strcpy(copy, current_command);
   char *args_t[MAX_ARGS];
   int nargs_t = get_args(copy, args_t);
   memcpy(commands[command_count], args_t, sizeof(args_t)*nargs_t); //Copy the command and it's arguments to the 2d array
   command_count++;
   current_command = strtok_r(NULL, "|\n", &cmdline_copy); //Use reentrant version of strtok to prevent fighting with get_args function
}
int fd[command_count*2-1];
pid_t pids[command_count];
for (int j = 0; j < command_count*2; j+=2) { //Open up a pair of pipes for every command
   pipe(fd+j);
}
for (int j = 0; j < command_count; j++) {
   pids[j] = fork();
   if (pids[j] == 0) { //Child process
      if (j == 0) { //Duplicate only stdout pipe for first pipe
         dup2(fd[1], fileno(stdout));
      }
      else if (j == (command_count-1)) { //Duplicate only stdin for last pipe
         up2(fd[2*(command_count-1)-2], fileno(stdin));
      }
      else { //Duplicate both stdin and stdout
         dup2(fd[2*(j-1)], fileno(stdin));
         dup2(fd[2*j+1], fileno(stdout));
      }
      for (int k = 0; k < j*2; k++) { //Close all fds
         close(fd[k]);
      }
      execvp(commands[j][0], commands[j]); //Exec the command
   }
   else if (pids[j] < 0) {
      perror("Error forking");
   }
}
for (int k = 0; k < command_count*2; k++) { //Parent closes all fds
   close(fd[k]);
}
waitpid(pids[command_count-1], NULL, 0); //Wait for only the last process;

您在 children 中(或者,在本例中,在 parent 中)没有关闭足够的文件描述符。

经验法则:如果你 dup2() 管道的一端连接到标准输入或标准输出,同时关闭 返回的原始文件描述符 pipe() 尽早。 特别是,您应该在使用任何 exec*() 函数族。

如果您使用以下任一方式复制描述符,则该规则也适用 dup() 或者 fcntl() F_DUPFD

在您的代码中,您在派生任何 children 之前创建了所有管道;因此,每个 child 都需要在复制一个或两个它将用于输入或输出的管道文件描述符后关闭所有管道文件描述符。

parent 进程还必须关闭所有管道描述符。

此外,parent 不应等待 children 完成,直到启动所有 children。一般来说,如果你按顺序 运行 children 将阻塞完整的管道缓冲区。您还会失去并行性的好处。但是请注意,parent 必须保持管道打开,直到它启动所有 children — 它不能在启动每个 child.

之后关闭它们。

对于你的代码,大纲操作应该是:

  • 创建 N 个管道
  • 对于 N(或 N+1)children 中的每一个:
    1. 分叉。
    2. Child复制标准输入和输出管道
    3. Child 关闭 所有 管道文件描述符
    4. Child执行流程(失败则报错退出)
    5. Parent 记录 child PID.
    6. Parent 继续下一次迭代;没有等待,没有关闭。
  • Parent 现在关闭 N 个管道。
  • Parent现在等待合适的children死亡。

还有其他组织方式,或多或少的复杂性。备选方案通常避免预先打开所有管道,从而减少要关闭的管道数量。

'Appropriate children' 表示有多种方法可以决定管道(由管道连接的命令序列)何时为 'done'.

  • 一个选项是等待序列中的最后一个命令退出。这有好处——而且是传统的做法。另一个优点是 parent 进程可以启动最后一个 child; child 可以在管道中启动它的前身,回到管道中的第一个进程。在这种情况下,parent 永远不会创建管道,因此它不必关闭任何管道。它也只有一个 child 等待;管道中的其他进程是 child.
  • 进程的后代
  • 另一种选择是等待所有进程结束(1)。这或多或少就是 Bash 所做的。这允许 Bash 知道管道每个元素的退出状态;替代方案不允许这样做——这与 set -o pipefailPIPEFAIL 数组有关。

Can you help me understand why the dup2 statement for the middle pipes is dup2(fd[(2*j)+1], fileno(stdout)) and dup2(fd[2*(j-1)], fileno(stdin))? I got it off Google and it works, but I'm unsure why.

  • fileno(stdout)1.
  • fileno(stdin)0.
  • 管道的读取端是文件描述符 0(类似于标准输入)。
  • 管道的写入端是文件描述符 1(类似于标准输出)。
  • 您有一个数组 int fd[2*N]; 用于某个 N > 1 的值,并且您为每个管道获得一对文件描述符。
  • 对于整数kfd[k*2+0]是管道的读描述符,fd[k*2+1]是读描述符。
  • j 既不是 0 也不是 (N-1) 时,您希望它从前一个管道读取并写入其管道:
    • fd[(2*j)+1] 是管道 j 的写描述符——它连接到 stdout.
    • fd[2*(j-1)] 是管道 j-1 的读取描述符——它连接到 stdin.
  • 因此,这两个 dup2() 调用将正确的管道文件描述符连接到管道中进程 j 的标准输入和标准输出。

(1) 在某些模糊的情况下,这会使 parent 无限期挂起。我强调晦涩难懂;它需要类似进程的东西,它作为守护进程存在而不分叉。