【问题标题】:Making stdin writable in a safe and portable way以安全和便携的方式使标准输入可写
【发布时间】:2014-11-02 06:55:33
【问题描述】:

我试图运行两个程序。

Case 1

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

int main()
{
        int n;
        int k = 10;
        int ret_val = 0;
        ret_val = write (0, &k, sizeof(int));
        if (-1 == ret_val)
        {
                printf ("Failed to write");
                exit (EXIT_FAILURE);
        }
        scanf ("%d", &n);
        printf ("Integer read is %d \n", n);
        return 0;
}

然后我尝试了下一个。

Case 2

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

int main()
{
        int n;
        int k = 10;
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open ("file_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == source_fd)
        {
                printf ("Failed to open file for reading");
                exit (EXIT_FAILURE);
        }
        int stdin_fd;

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        stdin_fd = dup (source_fd);
        if (-1 == stdin_fd)
        {
                printf ("Failed to dup");
                exit (EXIT_FAILURE);
        }

        /* write to stdin_buffer (content will be taken from file_in.txt) */
        ret_val = write (stdin_fd, &k, sizeof(int));
        if (-1 == ret_val)
        {
                printf ("Failed to write to stdin_buffer");
                exit (EXIT_FAILURE);
        }

        scanf ("%d", &n);
        printf ("Integer read is %d \n", n);

        close(source_fd);
        return 0;
}

现在在第一种情况下,我无法写入标准输入。在第二种情况下,我能够从文件“file_in.txt”中获取输入,并将内容发送到标准输入缓冲区。

我无法很好地解释为什么我的第一个案例没有成功。有人可以解释吗?

stdin 应该和其他文件一样吧?如果它是写保护的,很好。但是当我重定向输入时(在第二种情况下),没有“权限被拒绝”问题。此代码似乎不可移植。是否有一种可移植且安全的方式从文件重定向标准输入?


在通过 cmets 之后,我想出了一个更好的工作代码。我想要对此代码的一些反馈

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define LEN 100

int main()
{
        int n;
        char buffer[LEN];
        memset (buffer, '\0', LEN);
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open ("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
        {
                perror ("Failed to open file for reading");
                exit (EXIT_FAILURE);
        }
        /* Temp stdin_buffer */
        int temp_fd = open ("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
        {
                perror ("Failed to open temp stdin");
                exit (EXIT_FAILURE);
        }

        int stdin_fd;
        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        stdin_fd = dup (temp_fd);
        if (-1 == stdin_fd)
        {
                perror ("Failed to dup");
                exit (EXIT_FAILURE);
        }


        ret_val = read (source_fd, buffer, LEN);
        if (-1 == ret_val)
        {
                perror ("Failed to read from source");
                exit (EXIT_FAILURE);
        }
        else
        {
                printf ("%s read from Source file\n", buffer);
        }

        /* write to stdin_buffer (content taken from file_in.txt) */
        ret_val = write (stdin_fd, buffer, LEN);
        if (-1 == ret_val)
        {
                perror ("Failed to write to stdin_buffer");
                exit (EXIT_FAILURE);
        }


        ret_val = lseek (stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
        {
                perror ("Failed lseek");
                exit (EXIT_FAILURE);
        }

        ret_val = scanf ("%d", &n);
        if (-1 == ret_val)
        {
                perror ("Failed to read stdin_buffer");
                exit (EXIT_FAILURE);
        }
        printf ("Integer read is %d \n", n);

        close(source_fd);
        return 0;
}

【问题讨论】:

  • 它是特定于操作系统的。在 Linux 上,尝试strace 你的程序。
  • 操作系统特定或编译器依赖?那么有没有一种可移植的方式来写入标准输入?
  • 编译器只是从libstdc++libc 调用函数。而且没有可移植的方式写信给stdin。而且您的代码不会像您认为的那样做。在 Linux 上,阅读 pipe(7)
  • 您的第一个程序缺少一些#include 指令。 为什么你想写信给stdin
  • @KeithThompson 我试图使用execl() 运行可执行文件。在我完成这项工作后,我想在执行期间给它输入(就像我们在在线编码平台中所做的那样)。我的想法是将输入写入文件并将其重定向到stdin

标签: c stdin portability


【解决方案1】:

更新前

在第一个程序中,(可能)将 3 个空字节和一个换行符写入屏幕(不一定按此顺序);然后程序尝试从键盘读取(假设命令行上没有 I/O 重定向)。写入标准输入不会加载输入缓冲区。您经常可以写入标准输入(并从标准输出和标准错误中读取),因为经典技术使用 O_RDWR 打开文件描述符,然后将其连接到标准 I/O 通道。但是,不能保证您可以这样做。 (第一个程序需要&lt;unistd.h&gt;,顺便说一句。)

第二个程序有太多未定义的行为,很难分析。 open() 调用需要三个参数,因为它包括 O_CREAT;第三个参数是文件的模式(例如0644)。您不检查 open() 是否成功。您不检查写入是否成功;它不会,因为文件描述符打开O_RDONLY(或者,source_fd 打开O_RDONLYdup() 会将该模式复制到文件描述符0),这意味着write()将失败。不检查输入操作(您不能确保scanf() 成功)。 (第二个程序实际上不需要&lt;sys/types.h&gt;&lt;sys/stat.h&gt;。)

基本上,您对正在发生的事情一无所知,因为您没有检查任何关键的函数调用。

更新 1 后

请注意,错误消息应写入标准错误,并应以换行符终止。

我让第一个程序按规定工作(Mac OS X 10.10 Yosemite,GCC 4.8.1),尽管很难证明空字节已写入标准输入(但在那里写入了换行符)。然后我可以输入 10(或 20,或 100,或……)加上 Return,然后将打印该整数。

第二个程序在scanf() 上失败,因为当您尝试读取时文件指针位于文件末尾。你可以看到你的程序的这个变体:

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

int main(void)
{    
    /* Open file from which content shall be inserted to stdin_buffer */
    int source_fd = open ("file_in.txt", O_CREAT | O_RDWR, S_IRWXU);
    if (-1 == source_fd)
    {
        printf ("Failed to open file for reading\n");
        exit (EXIT_FAILURE);
    }

    close(0);
    int stdin_fd = dup (source_fd);
    if (-1 == stdin_fd)
    {
        printf("Failed to dup\n");
        exit(EXIT_FAILURE);
    }

    int k = 10;
    int ret_val = write(stdin_fd, &k, sizeof(int));
    if (-1 == ret_val)
    {
        printf("Failed to write to stdin_buffer\n");
        exit(EXIT_FAILURE);
    }

    int rc;
    int n;
    if ((rc = scanf("%d", &n)) != 1)
        printf("Failed to read from standard input: rc = %d\n", rc);
    else
        printf("Integer read is %d (0x%08x)\n", n, n);

    close(source_fd);
    return 0;
}

它产生:

Failed to read from standard input: rc = -1

如果你在阅读前回退文件,你会得到0返回;写入文件的二进制数据不是整数的有效字符串表示形式。

更新 2 后

我写了一个小函数err_exit(),因为它允许页面上的代码更小。我已经在几个地方修改了您的代码,以报告前一个函数的返回值。没有输入不是错误。当您读取 0 个字节时,这不是错误;它是EOF。当有数据要读取但不是整数值的文本格式时,不会发生转换,但这不是错误。

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

#define LEN 100

static void err_exit(const char *msg)
{
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void)
{
        int n = 99;
        char buffer[LEN];
        memset(buffer, '\0', LEN);
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin_buffer */
        int source_fd = open("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
                err_exit("Failed to open file for reading");
        /* Temp stdin_buffer */
        int temp_fd = open("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
                err_exit("Failed to open temp stdin");

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        int stdin_fd = dup(temp_fd);
        if (-1 == stdin_fd)
                err_exit("Failed to dup");

        ret_val = read(source_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to read from source");
        else
                printf("(%d bytes) <<%s>> read from Source file\n", ret_val, buffer);

        /* write to stdin_buffer (content taken from file_in.txt) */
        ret_val = write(stdin_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to write to stdin_buffer");

        ret_val = lseek(stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
                err_exit("Failed lseek");

        ret_val = scanf("%d", &n);
        if (-1 == ret_val)
                err_exit("Failed to read stdin_buffer");
        printf("Integer read is %d (ret_val = %d)\n", n, ret_val);

        close(source_fd);
        return 0;
}

输出:

(0 bytes) <<>> read from Source file
Integer read is 99 (ret_val = 0)

scanf() 无法读取值时,它(通常)不会将任何内容写入相应的变量。这就是99幸存下来的原因。如果您希望scanf() 可以将数据作为整数读取,则需要:

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

#define LEN 100

static void err_exit(const char *msg)
{
    perror(msg);
    exit(EXIT_FAILURE);
}

int main(void)
{
        int n = 99;
        char buffer[LEN] = "";
        int ret_val = 0;

        /* Open file from which content shall be inserted to stdin */
        int source_fd = open("file_in.txt", O_CREAT | O_RDONLY, S_IRWXU);
        if (-1 == source_fd)
                err_exit("Failed to open file for reading");
        /* Temp stdin */
        int temp_fd = open("temp_in.txt", O_CREAT | O_RDWR, S_IRWXU);
        if (-1 == temp_fd)
                err_exit("Failed to open temp stdin");

        /* Close STDIN_FILENO */
        close(0);
        /* dup the source */
        int stdin_fd = dup(temp_fd);
        if (-1 == stdin_fd)
                err_exit("Failed to dup");

        ret_val = read(source_fd, buffer, LEN);
        if (-1 == ret_val)
                err_exit("Failed to read from source");
        else
                printf("(%d bytes) <<%s>> read from Source file\n", ret_val, buffer);

        /* write to stdin (content taken from file_in.txt) */
        ret_val = write(stdin_fd, "10\n", sizeof("10\n")-1);
        if (-1 == ret_val)
                err_exit("Failed to write to stdin");

        ret_val = lseek(stdin_fd, 0, SEEK_SET);
        if (-1 == ret_val)
                err_exit("Failed lseek");

        ret_val = scanf("%d", &n);
        if (-1 == ret_val)
                err_exit("Failed to read stdin");
        printf("Integer read is %d (ret_val = %d)\n", n, ret_val);

        close(source_fd);
        return 0;
}

输出:

(0 bytes) <<>> read from Source file
Integer read is 10 (ret_val = 1)

【讨论】:

  • 我已更改代码。请仔细阅读并提供您的反馈。
  • 我已经用新代码 sn-p 更新了这个问题。我相信它在各个方面都是正确的。
【解决方案2】:

让我们看看你的三个程序在做什么......

计划 1

第一个程序写入文件描述符 0。默认情况下,这是 stdout,因此宏 STDOUT_FILENO 具有该值。由于stdout是单向流,跑进进程,写入失败。

方案 2

在这个程序中,您打开一个文件,它可能会得到 FD (filedescriptor) 3(在标准流的 0-2 之后)。然后,您使用 FD 0 关闭标准输出。然后,您 dup() FD 3 并且由于第一个打开点位于索引 0,这就是文件的新 FD。由于 scanf() 只使用(未更改的)宏 STDIN_FILENO,因此它将在那里获取该文件的内容。

方案 3

在程序 3 中,您的操作与程序 2 中的几乎相同,只是您打开文件两次。发生的事情的解释几乎遵循上述两个。

现在的问题就是您所说的“更好的工作代码”的意思。关键是,除非你指定你真正想要什么,否则不可能说“更好”。根据您的主题和评论猜测,您想远程控制第二个进程,就像在 bash 中使用管道符号一样。

为了做到这一点,您将不得不求助于特定于操作系统的方法,只需搜索“输入输出重定向”,您应该能够找到一些信息。由于您已经提到了 execl(),但 POSIX 之外的可移植性(例如到 win32)似乎不是问题,为此您应该找到很多示例代码,比我在这里写的要好得多。

无论如何,大致归结为以下步骤:

  • pipe() 创建一对 FD
  • fork()创建新进程
  • child:dup2() 将输入 FD 从 pipe() 重新分配给 stdin
  • child: execl() 新进程
  • 父进程:写入输出FD为子进程生成输入

此外,您应该关闭未使用的流或使用fcntl(FD_CLOEXEC) 配置它们以自动为您关闭它们。重要的一点是两个 FD 保持连接到同一个 (!) 通道,尽管您在两个进程中都有结束。在每个中关闭一个末端会在两个进程之间留下一个单向通道。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多