C语言中的管道在某些情况下会导致程序挂起

3
我正在尝试使用管道将一个命令的标准输出链接到另一个命令的标准输入。例如模仿(cmd1 | cmd2)。
以下是我代码的大幅简化版本。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main (int argC, char *argv[])
{

    //Run first command
    int fds[2];
    if (pipe (fds) < 0) {       //Create pipe
        fprintf (stderr, "Pipe Failed\n");
    }

    int pid;
    if ((pid = fork ()) == -1) {
        fprintf (stderr, "Fork 1 Failed\n");
        exit (1);
    }

    if (pid == 0) {             //First child proccess
        close (fds[0]);         //Close input end of pipe
        dup2 (fds[1], STDOUT_FILENO);   //Set stdout to output pipe
        close (fds[1]);         //Close output end of pipe

        fprintf (stderr, "Exec 1 executing now\n");
        execlp ("./addone", "./addone", NULL);  //execute first command - Doesnt cause hang
        //execlp("ls", "ls", NULL);//Causes hang
        fprintf (stderr, "Exec 1 failed\n");
    } else {                    //First parent segment
        int returnStatus;
        waitpid (pid, &returnStatus, 0);        //Wait for child 1 to finish

        fprintf (stderr, "Back to parent 1\n");
    }

    //Run second command
    if ((pid = fork ()) == -1) {
        fprintf (stderr, "Fork 2 failed\n");
    }

    if (pid == 0) {             //second child proccess
        dup2 (fds[0], STDIN_FILENO);    //Set stdin to input pipe
        close (fds[0]);         //Close input end of pipe
        close (fds[1]);         //Close output end of pipe

        fprintf (stderr, "Exec 2 executing now\n");
        execlp ("./addone", "./addone", NULL);  //execute first command - Doesnt cause hang
        //execlp("wc", "wc", NULL);//Causes hang
        fprintf (stderr, "Exec 2 failed\n");
    } else {                    //second parent segment
        int returnStatus;
        waitpid (pid, &returnStatus, 0);        //Wait for child 2 to finish

        fprintf (stderr, "Back to parent 2\n");
        //Done with pipes
        close (fds[0]);
        close (fds[1]);
    }
    return 0;
}

在这个程序中,我试图创建一个管道,将第一个执行文件的标准输出重定向到第二个执行文件的标准输入。当我尝试使用"ls | wc"作为我的执行文件时,程序在第二个执行文件处挂起。然而,当我使用一个简单的程序"./addone"作为我的执行文件时,整个执行过程成功完成。
其中,"addone"是一个小程序:
#include<stdio.h>

int main(){
    int value = 0;
    scanf("%d", &value);
    value++;
    printf("%d\n", value);
}

有人建议我的问题可能是由于输入管道被保持打开状态,但我无法确定发生在哪里,而且这并不能解释为什么当使用我的简单测试程序时,我的程序可以完美运行。

如果您能提供任何关于导致这个问题的原因的建议,将不胜感激。


2
您需要在父进程中关闭管道的两端。因为 pipefork 之前被调用,所以它在父进程和子进程中均打开。对于您的简单程序来说,这不是问题,因为它不像 wc 那样循环等待 EOF。只要管道仍由任何进程保持打开状态,EOF 就永远不会出现。 - kaylum
我不是已经用close(fds[0]); close(fds[1]);来做到了吗? 或者我应该把它们移到父进程的wait语句之前? - Killedan9
你确定你没有从程序中剥离掉重要的东西吗?在你发布的例子中,只要被exec的程序向stdout写入任何内容,我就会期望它在第一个waitpid处挂起——子进程中的printf将会阻塞,因为没有任何东西从管道的另一端读取,而父进程则会等待子进程而阻塞。建议:1)立即在父进程和子进程中关闭未使用的管道端(即在第一个fork之后关闭fds[1]等)。2)不要在启动第二个子进程之前等待第一个子进程,而是在最后等待两个子进程。 - davlet
@Killedan9 是的,在 waitpid 之前你必须这样做。否则子进程不会退出,而 waitpid 永远不会解除阻塞。 - kaylum
@davlet “子进程中的printf会阻塞,因为没有任何东西从管道的另一端读取”。我认为这是不正确的。也许你在想fifo。管道没有这种行为。而且,正如已经指出的那样,父进程两端都打开了。 - kaylum
显示剩余5条评论
1个回答

1
我不知道你是否解决了问题,但是继续评论,如果你要构建一个管道,关键是要理解:如果在一个进程中写入管道,则必须在下一个进程中从该同一管道中读取,并且如果要继续管道,则必须写入第二个管道。现在,您可以像链接C Minishell Adding Pipelines中所示那样自动化该过程,但是为了学习目的,如果您只是通过单个线性示例进行工作,那么理解起来可能更容易。
以下是使用两个文件描述符fd1和fd2实现您的代码。您的addone进程将首先从普通的stdin读取,因此对于其读取,没有操作文件描述符。但是,对于其写入,它必须使用fd1的“写端”(缺乏更好的词语)将其输出管道传递到下一个子进程。
下一个子进程必须从相同的fd1的输入端读取以接收其输入,并且它必须写入另一个文件描述符fd2的输出端。在两个子进程都退出后,父进程必须从哪里读取?(最后一个管道写到哪里了...?啊哈!)fd2。因此,父进程复制fd2的读端(关闭所有其他未使用的端口),并读取有多少个addone添加到初始数字的最终答案。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main (void) {

    int fd1[2], fd2[2];  /* file descriptors 1 & 2 */
    int pid;

    if (pipe (fd1) < 0 || pipe (fd2) < 0) { /* open both pipes */
        fprintf (stderr, "pipe creation failed\n");
    }

    if ((pid = fork ()) == -1) {
        fprintf (stderr, "fork 1 failed\n");
        exit (1);
    }

    if (pid == 0) {                     /* first child */
        dup2 (fd1[1], STDOUT_FILENO);   /* dup write-end of 1st */
        close (fd1[0]);                 /* close all others */
        close (fd2[0]);
        close (fd2[1]);

        fprintf (stderr, "Exec 1 executing now\n");
        execlp ("./addone", "./addone", NULL);
        fprintf (stderr, "Exec 1 failed\n");
    }
    else {
        int returnStatus;
        waitpid (pid, &returnStatus, 0);
        fprintf (stderr, "Back to parent 1\n");
    }

    if ((pid = fork ()) == -1) {
        fprintf (stderr, "Fork 2 failed\n");
    }

    if (pid == 0) {                     /* second child */
        dup2 (fd1[0], STDIN_FILENO);    /* dup read-end of 1st  */
        dup2 (fd2[1], STDOUT_FILENO);   /* dup write-end of 2nd */
        close (fd1[1]);                 /* close all others */
        close (fd2[0]);

        fprintf (stderr, "Exec 2 executing now\n");
        execlp ("./addone", "./addone", NULL);
        fprintf (stderr, "Exec 2 failed\n");
    }
    else {
        int returnStatus;
        waitpid (pid, &returnStatus, 0);
        fprintf (stderr, "Back to parent 2\n");
    }

    dup2 (fd2[0], 0);   /* dup read-end of 2nd */
    close (fd2[1]);     /* close all others */
    close (fd1[0]);
    close (fd1[1]);

    int total;
    scanf ("%d", &total);
    printf ("The total was: %d\n\n", total);

    close (fd2[0]);

    return 0;
}

示例使用/输出
$ echo 10 | ./bin/pipeaddone
Exec 1 executing now
Back to parent 1
Exec 2 executing now
Back to parent 2
The total was: 12

请仔细阅读并告诉我你是否有任何问题。一旦你理解了它,就会很容易。如果你有一段时间没有使用它,不用担心,你会再次重新学习它 :)


感谢您详细的回复!我已经成功让我的程序运行起来了,现在我终于明白管道是如何工作的了。 - Killedan9
很高兴能够帮忙。距离我上一次使用这种方法编写代码已经有一年了,所以我又可以重新享受学习的过程了 :) - David C. Rankin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接