【问题标题】:C Shell hanging when dealing with pipingC 处理管道时挂壳
【发布时间】:2018-04-20 03:53:10
【问题描述】:

我正在使用 C 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;

【问题讨论】:

  • fd 数组和两个for 循环肯定有问题:第一个循环使用fd 中的i*2 元素,但第二个循环仅使用i+1 元素.至少第二个循环应该有一个j+=2 增量。可悲的是现在没有时间进一步看,这只是一个线索,
  • 那不是因为管道有两个末端吗?对于 2 个命令,您有 4 个管道,2 进 2 出,然后第二个 for 循环遍历这两个命令。
  • 在某种程度上,可以编辑问题。一旦你开始得到答案,你应该谨慎地改变问题以使答案无效。通常,这可能意味着从那里单独保留原始代码(除非您修复非关键拼写错误或类似内容),但添加新/修订代码作为编辑。你应该展示一些有缺陷的代码版本——也可以添加工作代码,但不能使用适当的注释。

标签: c pipe fork exec dup


【解决方案1】:

您没有在子级(或者,在这种情况下,在父级)中关闭足够的文件描述符。

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

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

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

父进程也必须关闭所有管道描述符。

此外,父级不应等到子级完成后才启动所有子级。一般来说,如果你让子进程按顺序运行,它们会被完整的管道缓冲区阻塞。您还破坏了并行性的好处。但是请注意,在启动所有子节点之前,父节点必须保持管道打开 - 在启动每个子节点后不得关闭管道。

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

  • 创建 N 个管道
  • 对于 N(或 N+1)个孩子中的每一个:
    1. 叉子。
    2. 子级复制标准输入和输出管道
    3. 子关闭所有管道文件描述符
    4. 子进程执行(失败报错退出)
    5. 父记录子 PID。
    6. 父级继续下一次迭代;无需等待,无需关闭。
  • 父级现在关闭 N 个管道。
  • 父母现在等待适当的孩子死亡。

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

“适当的孩子”意味着有多种方法可以确定管道(由管道连接的命令序列)何时“完成”。

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

你能帮我理解为什么中间管道的dup2 语句是dup2(fd[(2*j)+1], fileno(stdout))dup2(fd[2*(j-1)], fileno(stdin)) 吗?我从 Google 上得到它,它可以工作,但我不确定为什么。

  • 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) 可能有一些模糊的场景,这会使父级无限期地挂起。我强调晦涩;它需要像一个进程一样作为守护进程挂起而不分叉。

【讨论】:

  • 我注释掉了等待语句并关闭了所有文件描述符(见编辑)。现在它没有挂起,但它也没有输出任何东西。
  • 查看更新的答案。在您更新的代码中,似乎父级关闭了子启动循环内的管道。那是错误的地方。
  • 要明确一点,是不是 N 个管道,1 进 1 出,总共 N*2?还是只有 N 个?
  • 一个管道有两个文件描述符; N 个管道有 2N 个文件描述符。当父级关闭 N 个管道时,它会关闭 2N 个文件描述符。当子进程关闭所有管道文件描述符时,它会关闭 2N 个文件描述符。
  • 非常感谢!我得到了它。你能帮我理解为什么中间管道的 dup2 语句是 fd[(2*j)+1], fileno(stdout)fd[2*(j-1)], fileno(stdin),我从谷歌上得到它并且它有效,但我不确定为什么。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-22
  • 2022-10-04
  • 1970-01-01
  • 2018-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多