首页 > 解决方案 > 使用管道后恢复标准输出的问题

问题描述

使用此处找到的建议:使用 dup 后恢复标准输出 我尝试恢复标准输入和标准输出。但是,当使用 printf 检查 stdout 是否已恢复时,我无法读取输出。代码如下。将标准输出恢复为 1 后,我尝试打印“完成”。

#include <stdio.h>
#include <unistd.h>

void main(int argv, char *argc) {
    int stdin_copy = dup(STDIN_FILENO);
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe); 
    int PID = fork();
    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    } else {
        dup2(testpipe[1], 1);
        close(testpipe[0]);
        printf("5");
        fclose(stdout);
        close(testpipe[1]);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        fprintf(stderr, "initial value: %s\n", initialval);
        wait(NULL);
        dup2(stdin_copy, 0);
        dup2(stdout_copy, 1);
        printf("done");//was not displayed when I run code.
    }
}

但是,当我运行代码时,我没有看到“完成”。(应该在15点之后完成)。这是我的输出:初始值:exec成功a:3 b:5 multby成功15

恢复标准输出时我做错了什么?

标签: cpipestdoutdup2dup

解决方案


一般说明

在其他答案中重申我之前在 SO 上所说的话。

您没有在子进程中关闭足够的文件描述符。


经验法则:如果您 将管道的一端连接到标准输入或标准输出,请 尽快dup2() 关闭返回的两个原始文件描述符 。pipe()特别是,您应该在使用任何 exec*() 函数系列之前关闭它们。

如果您使用 或 或 复制描述符,该 规则也dup() 适用 。fcntl()F_DUPFDF_DUPFD_CLOEXEC


如果父进程不会通过管道与其任何子进程通信,它必须确保它足够早地关闭管道的两端(例如,在等待之前),以便其子进程可以在读取时接收 EOF 指示(或获取 SIGPIPE信号或写入错误),而不是无限期地阻塞。即使父级使用管道而不使用dup2(),它通常也应该至少关闭管道的一端——程序在单个管道的两端读取和写入的情况极为罕见。

请注意,选项O_CLOEXECto open()和选项 to也可以作为讨论的因素。FD_CLOEXECF_DUPFD_CLOEXECfcntl()

如果您使用 及其广泛的支持函数系列(总共 21 个函数),您将需要查看如何在生成的进程(等)posix_spawn() 中关闭文件描述符。posix_spawn_file_actions_addclose()

请注意, 出于各种原因,使用dup2(a, b)比使用更安全。close(b); dup(a);一个是,如果你想强制文件描述符大于通常的数字,dup2()这是唯一明智的方法。另一个是 ifab(例如 both 0)相同,则正确处理它(在复制之前dup2() 它不会关闭),而分离并可怕地失败。这是一个不太可能但并非不可能的情况。baclose()dup()


代码分析

问题包含代码:

#include <stdio.h>
#include <unistd.h>

void main(int argv, char *argc) {
    int stdin_copy = dup(STDIN_FILENO);
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe); 
    int PID = fork();
    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    } else {
        dup2(testpipe[1], 1);
        close(testpipe[0]);
        printf("5");
        fclose(stdout);
        close(testpipe[1]);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        fprintf(stderr, "initial value: %s\n", initialval);
        wait(NULL);
        dup2(stdin_copy, 0);
        dup2(stdout_copy, 1);
        printf("done");//was not displayed when I run code.
    }
}

该行void main(int argv, char *argc) {应该是int main(void)因为您不使用命令行参数。您也有名称argv,并且argc与正常约定相反——第一个参数通常称为argc(参数计数),第二个通常称为argv(参数向量)。此外,第二个参数的类型应该是char **argv(或者char **argc如果你想混淆所有的普通读者)。另请参阅C 和 C++ 中应该返回什么?main()

下一个值得讨论的代码块是:

    if (PID == 0) { 
        dup2(testpipe[0], 0);
        close(testpipe[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
    }

这打破了经验法则。您还应该将错误处理代码放在execl().

if (PID == 0)
{
    dup2(testpipe[0], STDIN_FILENO);
    close(testpipe[0]);
    close(testpipe[1]);
    execl("./multby", "multby", "3", (char *)NULL);
    fprintf(stderr, "failed to execute ./multby\n");
    exit(EXIT_FAILURE);
}

下一个要分析的代码块是:

dup2(testpipe[1], 1);
close(testpipe[0]);
printf("5");
fclose(stdout);
close(testpipe[1]);

理论上,你应该使用STDOUT_FILENO代替1,但我对使用 of 有相当的同情1(尤其是因为当我第一次学习 C 时,并没有这样的符号常数)。实际上,您确实关闭了管道的两端,但我希望dup2()根据经验法则,在通话后立即关闭两者。printf()没有换行符确实会发送任何内容;它将5存放在 I/O 缓冲区中。

正如克里斯·多德(Chris Dodd )在他们的回答中指出的那样,这个fclose(stdout)电话是很多麻烦的来源。您可能应该简单地将其替换为fflush(stdout).

继续:

char initialval[100];
read(testpipe[0], initialval, 100);
fprintf(stderr, "initial value: %s\n", initialval);
wait(NULL);
dup2(stdin_copy, 0);
dup2(stdout_copy, 1);
printf("done");

你没有检查是否read()有效。它没有;EBADF 失败了,因为在此之上您使用close(testpipe[0]);. 你在stderr那里打印的是一个未初始化的字符串——这不好。在实践中,如果你想可靠地从孩子那里读取信息,你需要两个管道,一个用于父子通信,另一个用于子父通信。否则,不能保证父母不会阅读它所写的内容。如果您在阅读之前等待孩子死去,那么您将有很大的机会让它发挥作用,但您不能总是依靠能够做到这一点。

这两个dup2()电话中的第一个是没有意义的。您没有更改标准输入的重定向(因此实际上stdin_copy是不必要的)。第二个更改了标准输出文件描述符的分配,但是您已经关闭了stdout,因此没有简单的方法可以重新打开它以使其printf()正常工作。消息应该以换行符结尾——大多数printf()格式字符串应该以换行符结尾,除非它被故意用于构建单行输出。但是,如果您注意返回值,您会发现它失败了(-1)并且您很有可能errno == EBADF再次找到它。

修复代码——脆弱的解决方案

鉴于此代码multby.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage; %s number", argv[0]);
        return 1;
    }
    int multiplier = atoi(argv[1]);
    int number;
    if (scanf("%d", &number) == 1)
        printf("%d\n", number * multiplier);
    else
        fprintf(stderr, "%s: failed to read a number from standard input\n", argv[0]);
    return 0;
}

和这段代码(如pipe23.c,编译为pipe23):

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int stdout_copy = dup(STDOUT_FILENO);
    int testpipe[2];
    pipe(testpipe);
    int PID = fork();
    if (PID == 0)
    {
        dup2(testpipe[0], 0);
        dup2(testpipe[1], 1);
        close(testpipe[1]);
        close(testpipe[0]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
        fprintf(stderr, "failed to execute ./multby (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    else
    {
        dup2(testpipe[1], 1);
        close(testpipe[1]);
        printf("5\n");
        fflush(stdout);
        close(1);
        wait(NULL);
        char initialval[100];
        read(testpipe[0], initialval, 100);
        close(testpipe[0]);
        fprintf(stderr, "initial value: [%s]\n", initialval);
        dup2(stdout_copy, 1);
        printf("done\n");
    }
}

这种组合几乎不起作用——它不是一个有弹性的解决方案。例如,我在5. 孩子在 之后等待另一个字符5来确定它已经读完这个数字。它没有得到 EOF,因为它打开了管道的写端以将响应发送给父级,即使它挂起从管道读取,所以它永远不会写入它。但是因为它只尝试读取一个数字,所以没关系。

输出是:

initial value: [15
]
done

修复代码——强大的解决方案

如果您要处理任意数量的数字,则需要使用两个管道——这是完成任务的唯一可靠方法。当然,这也适用于传递给孩子的单个数字。

这是一个修改后multby.c的循环阅读:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage; %s number", argv[0]);
        return 1;
    }
    int multiplier = atoi(argv[1]);
    int number;
    while (scanf("%d", &number) == 1)
        printf("%d\n", number * multiplier);
    return 0;
}

这是一个修改pipe23.c,它使用两个管道并将 3 个数字写入孩子,并返回三个结果。请注意,它不需要在该组织的第三个数字之后放置换行符(尽管如果它确实包含换行符也不会造成任何伤害)。此外,如果你是狡猾的,数字列表中的第二个空格也是不必要的;不是第二个数字的-一​​部分,因此扫描在0.

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int stdout_copy = dup(STDOUT_FILENO);
    int p_to_c[2];
    int c_to_p[2];
    if (pipe(p_to_c) != 0 || pipe(c_to_p) != 0)
    {
        fprintf(stderr, "failed to open a pipe (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    int PID = fork();
    if (PID == 0)
    {
        dup2(p_to_c[0], 0);
        dup2(c_to_p[1], 1);
        close(c_to_p[0]);
        close(c_to_p[1]);
        close(p_to_c[0]);
        close(p_to_c[1]);
        execl("./multby", "multby", "3", NULL);// Just multiplies argument 3 with the number in testpipe.
        fprintf(stderr, "failed to execute ./multby (%d: %s)\n", errno, strerror(errno));
        exit(EXIT_FAILURE);
    }
    else
    {
        dup2(p_to_c[1], 1);
        close(p_to_c[1]);
        close(p_to_c[0]);
        close(c_to_p[1]);
        printf("5 10 -15");
        fflush(stdout);
        close(1);
        char initialval[100];
        int n = read(c_to_p[0], initialval, 100);
        if (n < 0)
        {
            fprintf(stderr, "failed to read from the child (%d: %s)\n", errno, strerror(errno));
            exit(EXIT_FAILURE);
        }
        close(c_to_p[0]);
        wait(NULL);
        fprintf(stderr, "initial value: [%.*s]\n", n, initialval);
        dup2(stdout_copy, 1);
        printf("done\n");
    }
}

请注意,其中有很多调用close()——处理两个管道所涉及的 4 个描述符中的每一个都有两个调用。这个是正常的。不注意关闭文件描述符很容易导致系统挂起。

运行它的输出pipe23是这样的,这就是我想要的:

initial value: [15
30
-45
]
done

推荐阅读