使用 scanf 从管道读取失败

Posted

技术标签:

【中文标题】使用 scanf 从管道读取失败【英文标题】:failed using scanf to read from pipe 【发布时间】:2018-12-09 15:44:15 【问题描述】:

在 IPC 上工作,我被要求编写一个 C 程序,作为其他两个 C 可执行文件之间的管道:

名为“sln1.out”的第一个可执行文件接收六个参数并打印三个数字。

名为“sln2.out”的第二个可执行文件接收三个参数并打印一个数字。

我将以下代码分为两部分——第一部分是写入管道,据我所知它可以工作。问题从第二部分开始:我关闭了stdin,所以现在当我使用dup(fd[0]) 时,新的文件描述符副本应分配到stdin 所在的位置,我想我可以使用scanf 从在这种情况下管道 - 但由于某种原因它不起作用

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])

    // check number of arguments.
    if(argc != 7)
    
            printf("Wrong parameters");
            exit(1);
    

    // creating the pipe.
    int fd[2];
    pipe(fd);

/* PART ONE: forking a child for 'sln1.out' that writes to fd[1] */

    // I  want to fork this process, and change the image of the child process to the 'sln1.out' process.
    pid_t pid_sln1 = fork();
    int sln1_status;
    if (pid_sln1 < 0)
    
            perror("fork error, sln1");
    
    else if(pid_sln1 == 0)
    
            char* const parmListSln1[] = "./sln1.out",argv[1],argv[2],argv[3],
                                            argv[4],argv[5],argv[6],NULL;
            // i closed the stdout, and used 'dup' that return the file descriptor
            //  of stdout as duplicate of fd[1]!
            close(STDOUT_FILENO);
            dup(fd[1]);

            execv("./sln1.out",parmListSln1);
            printf("Return not expected, exacv error.\n");
            exit(1);
    

    // wait untill the child process terminated.
    wait(&sln1_status);
    if(sln1_status == 0)
    
            printf("child process terminated successfully\n");
            // if we want to read from fd[0] we must close the write to fd[1]

            close(fd[1]);
    
    else
    
            printf("child process failed\n");
            exit(1);
    


/* PART TWO: forking a child for 'sln2.out' that reads from fd[0] */

    // The same idea - forking a child to change its image to the 'sln2.out' process.
    pid_t pid_sln2 = fork();
    int sln2_status;

    if(pid_sln2 < 0)
    
            printf("fork error, sln2.\n");
            exit(1);
    
    else if(pid_sln2 == 0)
    
            // closing 'stdin' and the use fo 'dup' create a duplicate to the readable
            // side of the pipe where the standard input should be
            close(STDIN_FILENO);
            dup(fd[0]);

            // reading the input from the pipe - with the same method used to 'stdin'!
            char* in[3];
            scanf("%s %s %s",in[0],in[1],in[2]);
            // build the parameters list for 'sln2.out'
            char* const paramListSln2[] =  "./sln2.out", in[0], in[1], in[2], NULL ;

            // execute 'sln2.out'
            execv("./sln2.out",paramListSln2);
            printf("Return not expexted, execv error");
            exit(1);

    

    // wait untill the child process terminated and determine success.
    wait(&sln2_status);
    if (sln2_status == 0)
    
            printf("2nd child process terminated successfully!\n");
            exit(0);
    
    else
    
            printf("error with 'sln2.out' child process.\n");
            exit(1);
    

    exit(0);

我得到的输出可以提供更多细节:

child process terminated successfully
error with 'sln2.out' child process.

我很确定sln2.out 进程的问题是因为scanf 失败,因为我尝试打印扫描的参数并且它也失败了......

【问题讨论】:

如果你正在编写所有 3 个可执行文件,sln1、sln2 和上面显示的包装器,它们是紧密集成的。那么最好将它们放入一个可执行文件(无执行程序)中。我还注意到您在开始 2 之前等待 1 完成,如果管道填满,这将导致问题。 首先我正在编写所有三个程序,但我必须保持这种结构,因为这是大学要求的......我看不出管道会填满的真正原因,如果我得到这对管道最多可以容纳 4KB 对吗?而且这里的输入/输出非常小。 是的,管道可以容纳超过 4 KiB,但在“现实世界”中填充管道是一个问题(大学练习之外)。记住这一点。您在这两个可执行文件中添加了哪些诊断信息?您是否将参数列表等打印到stderr?您是否也将它们生成的任何数字打印到stderr?当事情没有按计划进行时(就像现在,鉴于您正在提出问题),那么仔细诊断输出跟踪哪个进程正在生成什么输出等变得至关重要(或者,如果不是至关重要的话,非常有帮助)。还要确保您测试每个系统调用以确保其正常工作。 连续的行char* in[3]; scanf("%s %s %s",in[0],in[1],in[2]); 是崩溃的来源——指针不指向任何地方。您需要为指向的指针分配存储空间,或使用m 修饰符对%s 和其他相应的更改,以便scanf() 为您分配内存。请注意,某些系统(例如 macOS)不支持 POSIX 强制的 sscanf() 修饰符。使用char in[3][50]; 可能是最简单的(尽管您应该在scanf() 格式字符串中每次都使用%49s。) 我不知道这是唯一的问题;这是我的编译器在尝试编译您的代码时告诉我的一个问题。我调用了你的程序ctrl61.c 并(试图)编译:gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes ctrl61.c -o ctrl61——编译器拒绝编译它。 【参考方案1】:

主要问题——未初始化的指针

当我使用命令行编译问题中的代码(源文件,ctrl61.c)时:

gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes ctrl61.c -o ctrl61

(在装有 macOS 10.14.2 Mojave 的 Mac 上运行 GCC 8.2.0),我收到如下警告:

ctrl61.c:78:13: error: ‘in[0]’ may be used uninitialized in this function [-Werror=maybe-uninitialized]

对于in[0]in[1]in[2] 中的每一个,标识的行是对scanf() 的调用。未初始化的指针是崩溃的根源,实际上,检查代码表明指针未初始化。 您需要为指向的指针分配存储空间。最简单的改变是使用:

char in[3][50];

(尽管您应该在 scanf() 格式字符串中每次都使用 %49s。) 或者您可以使用m 修饰符对%s 和其他相应的更改,以便scanf() 为您分配内存。请注意,某些系统(例如 macOS)不支持 POSIX 强制的 sscanf() 修饰符。

您没有在子级(或者实际上,在父级)中关闭足够的文件描述符。

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

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

在这个程序中,这可能无关紧要,但如果您更普遍地使用管道,确保关闭所有未使用的管道通常至关重要,因为进程可能不会在需要时获得 EOF。

错误报告

在 cmets 中,您提到使用 perror() 报告问题。就个人而言,我不喜欢perror() 报告错误;它的格式不够强大。但是,它比一些替代品要好。

我通常使用 GitHub 上我的SOQ(堆栈溢出问题)存储库中的一些代码作为文件stderr.cstderr.h 位于src/libsoq 子目录中。这对格式有广泛的控制。

在 Linux 和 BSD(包括 macOS)上有一个概念上相似的包 err(3)。我更喜欢我的,只是因为它是我的(而且它比err(3) 包有更强大的控制)。

控制码ctrl61.c

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

int main(int argc, char *argv[])

    if (argc != 7)
    
        fprintf(stderr, "Usage: %s arg1 arg2 arg3 arg4 arg5 arg6\n", argv[0]);
        exit(1);
    

    int fd[2];
    pipe(fd);

    pid_t pid_sln1 = fork();
    int sln1_status;
    if (pid_sln1 < 0)
    
        perror("fork error, sln1");
    
    else if (pid_sln1 == 0)
    
        char *paramListSln1[] =
        
            "./sln1.out", argv[1], argv[2], argv[3],
            argv[4], argv[5], argv[6], NULL
        ;

        close(STDOUT_FILENO);
        dup(fd[1]);
        close(fd[0]);
        close(fd[1]);

        execv(paramListSln1[0], paramListSln1);
        fprintf(stderr, "%s: failed to exec %s\n", argv[0], paramListSln1[0]);
        exit(1);
    

    pid_t pid_sln2 = fork();
    int sln2_status;

    if (pid_sln2 < 0)
    
        printf("fork error, sln2.\n");
        exit(1);
    
    else if (pid_sln2 == 0)
    
        close(STDIN_FILENO);
        dup(fd[0]);
        close(fd[0]);
        close(fd[1]);

        char in[3][50];
        scanf("%49s %49s %49s", in[0], in[1], in[2]);

        char *const paramListSln2[] =  "./sln2.out", in[0], in[1], in[2], NULL ;

        execv(paramListSln2[0], paramListSln2);
        fprintf(stderr, "%s: failed to exec %s\n", argv[0], paramListSln2[0]);
        exit(1);
    

    close(fd[0]);
    close(fd[1]);

    int pid1 = wait(&sln1_status);
    if (sln1_status == 0)
    
        fprintf(stderr, "child process %d terminated successfully\n", pid1);
        close(fd[1]);
    
    else
    
        fprintf(stderr, "child process %d failed 0x%.4X\n", pid1, sln1_status);
        exit(1);
    

    int pid2 = wait(&sln2_status);
    if (sln2_status == 0)
    
        fprintf(stderr, "child process %d terminated successfully\n", pid2);
        close(fd[1]);
    
    else
    
        fprintf(stderr, "child process %d failed 0x%.4X\n", pid2, sln2_status);
        exit(1);
    

    return(0);

这段代码中有明显的重复,应该通过编写函数来修复。

请注意,此版本会先启动两个程序,然后再等待其中一个程序退出。

辅助程序sln1.out.c

这与注释中假设的代码密切相关,但修复了注释使用 argv[1] 但本应使用 argv[0] 的错误。

#include <stdio.h>

static inline void dump_args(int argc, char **argv)

    int argnum = 0;
    fprintf(stderr, "%s: %d arguments\n", argv[0], argc);
    while (*argv != 0)
        fprintf(stderr, "%d: [%s]\n", argnum++, *argv++);


int main(int argc, char **argv)

    dump_args(argc, argv);
    if (argc != 7)
    
        fprintf(stderr, "%s: incorrect argument count %d\n", argv[0], argc);
        return(1);
    
    printf("1 2 3\n");
    return(0);

程序 sln2.out.c 的不同之处在于需要 3 个参数并打印 321 而不是 1 2 3

示例运行

$ ./ctrl61 abc zoo def pqr tuv 999
./sln1.out: 7 arguments
0: [./sln1.out]
1: [abc]
2: [zoo]
3: [def]
4: [pqr]
5: [tuv]
6: [999]
child process 15443 terminated successfully
./sln2.out: 4 arguments
0: [./sln2.out]
1: [1]
2: [2]
3: [3]
321
child process 15444 terminated successfully
$

这表明sln2.out 传递了三个从sln1.out 的标准输出中读取的参数。

【讨论】:

以上是关于使用 scanf 从管道读取失败的主要内容,如果未能解决你的问题,请参考以下文章

在 Dataflow 上运行的 Apache Beam 管道无法从 KafkaIO 读取:SSL 握手失败

使用 ReadFile 异步读取管道

在这种情况下使用管道会导致 write() 失败吗?

尝试使用管道读取/写入另一个程序

来自子项的scanf扫描父母已经扫描的内容

如何从 GCP 存储桶中读取 Apache Beam 中的多个文件