【问题标题】:Problem restoring stdout after using pipe使用管道后恢复标准输出的问题
【发布时间】:2024-04-23 04:10:01
【问题描述】:

使用此处的建议:Restoring stdout after using 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点之后完成)。 这是我的输出: 初始值: 执行成功 一个:3 乙:5 多次成功 15

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

【问题讨论】:

    标签: c pipe stdout dup2 dup


    【解决方案1】:

    您正在调用fclose(stdout);,它关闭了标准输出FILE 对象以及STDOUT_FILELO——标准输出文件描述符(它们是两个不同的东西,链接在一起)。因此,当您稍后调用dup2(stdout_copy, 1); 时,它会恢复文件描述符,但FILE 对象仍处于关闭状态,因此您无法使用它来输出任何内容。没有办法重新打开 FILE 对象来引用文件描述符1,所以最好的办法就是删除 fclose 行。 dup2 将关闭您要替换的文件描述符(因此您实际上不需要单独关闭)并且您应该能够看到输出


    1您可以在某些系统上将freopen 与/dev/fd 一起使用,但/dev/fd 是不可移植的

    【讨论】:

    • 由于freopen() 是标准C 的一部分,它非常便携。由于fdopen() 是标准 POSIX 的一部分,因此它也具有相当的可移植性——如果目标机器上有任何 POSIX 可用的话。
    • @JonathanLeffler 但 /dev/fd 不是标准
    • 啊——我明白了。您声称的不可移植与我假设您所说的不同。也许“那个”应该是“/dev/fd”?
    • 但是是否不需要 fclose() 将输入 (5) 刷新到管道中?否则我没有从 dup(testpipe[0], 0) 得到任何输入
    • 取决于你的缓冲模式——如果缓冲,使用 fflush(stdout) 刷新。
    【解决方案2】:

    一般说明

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

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


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

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


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

    请注意,O_CLOEXEC 选项 open(), 和FD_CLOEXECF_DUPFD_CLOEXEC 选项到fcntl() 也可以考虑 进入这个讨论。

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

    请注意,使用dup2(a, b) 比使用close(b); dup(a); 更安全 出于各种原因。 一种是如果你想强制文件描述符大于 通常的号码,dup2() 是唯一明智的做法。 另一个是如果ab 相同(例如两者都是0),那么dup2() 正确处理它(在复制 a 之前它不会关闭 b) 而单独的 close()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),因为您不使用命令行参数。您还可以将名称 argvargc 与正常约定相反——第一个参数通常称为 argc(参数计数),第二个通常称为 argv(参数向量)。此外,第二个参数的类型应该是char **argv(或者char **argc,如果你想迷惑所有的普通读者)。另见What should main() return in C and C++?

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

        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,但我对1的使用相当同情(尤其是因为当我第一次学习C时,并没有这样的符号常量)。实际上,您确实关闭了管道的两端,但根据经验法则,我希望在 dup2() 调用之后立即关闭两者。没有换行符的printf() 确实会发送任何内容;它将5 隐藏在 I/O 缓冲区中。

    正如Chris Dodd 在他们的answer 中标识的那样,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
    

    【讨论】:

      最近更新 更多