【问题标题】:How can I read from a stream without waiting for newline?如何在不等待换行符的情况下从流中读取?
【发布时间】:2013-07-30 16:50:05
【问题描述】:

我正在尝试用 C 语言编写一个简单的控制台程序,它分叉、运行一个子进程,并使用 pipe() 读取父程序的所有标准输入并将其发送到子程序的标准输入并读取所有标准输出子程序并将其发送到父程序的标准输出。最终,我可以让父程序做一些事情,而不仅仅是传递这些数据——现在,如果父程序的行为就像子程序直接运行一样就足够了。

似乎工作正常,除了当我从子输出管道传输的流中读取()时,它不会返回任何内容,直到遇到换行符。这意味着如果子进程在一行中间提出问题,则直到用户键入答案后,该问题才会出现,这是不可接受的。

我对父母的标准输入也有同样的问题。即使用户应该能够用简单的“y”回答问题,但在用户按下回车之前程序无法读取“y”,这也是不可接受的。

我将输入流设置为非阻塞:

fcntl(stream, F_SETFL, fcntl(stream, F_GETFL) | O_NONBLOCK);

它工作正常,但 read() 返回 -1 直到遇到换行符。

我可以做些什么来从流中读取实际数据而不受部分行的“保护”吗?还是我应该采取一些完全不同的方法?是否有一些开源程序可以做类似的事情,我可以检查一下?

代码在这里:

#include <unistd.h>
#include <sched.h>
#include <fcntl.h>

int main(int argc, const char * const argv[])
{
    int outfd[2];
    int infd[2];

    int oldstdin, oldstdout;

    pipe2(outfd, O_NONBLOCK); // Where the parent is going to write to
    pipe2(infd, O_NONBLOCK); // From where parent is going to read

    oldstdin = dup(0); // Save current stdin
    oldstdout = dup(1); // Save stdout

    close(0);
    close(1);

    dup2(outfd[0], 0); // Make the read end of outfd pipe as stdin
    dup2(infd[1],1); // Make the write end of infd as stdout

    if(!fork())
    {
        const char * pChildArguments[] = { "/usr/bin/php", "test.php", 0 };
        close(outfd[0]); // Not required for the child
        close(outfd[1]);
        close(infd[0]);
        close(infd[1]);

        execv(pChildArguments[0], (char * const *)pChildArguments);
    }
    else
    {
        char input[100];
        close(0); // Restore the original std fds of parent
        close(1);
        dup2(oldstdin, 0);
        dup2(oldstdout, 1);

        close(outfd[0]); // These are being used by the child
        close(infd[1]);

        fcntl(infd[0], F_SETFL, fcntl(infd[0], F_GETFL) | O_NONBLOCK);
        fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);

        for (;;) {
            ssize_t readReturnValue;

            readReturnValue = read(infd[0], input, 100);
            if (readReturnValue == 0) { break; }
            if (readReturnValue > 0) {
                write(1, input, readReturnValue);
                fsync(1);
            }
            readReturnValue = read(0, input, 100);
            if (readReturnValue > 0) {
                write(outfd[1], input, readReturnValue);
                fsync(outfd[1]);
            }
            sched_yield();
        }
    }
}

改编自this blog post

test.php(用作子进程)是这样的:

<?php
echo "This lines goes through.\n";
$a = readline("Say something: ");
echo "You said " . $a . "\n";

【问题讨论】:

  • 您的孩子会为管道调用 fflush 吗?
  • 你确定是接收过程出错,而不是发送方等待换行后再发送吗?您能否编辑您的问题以包含SSCCE,以便我们可以看到您在做什么?
  • 我无法控制子进程的编程方式,除了上面的简单测试用例。这个父程序应该适用于任何子程序,并且它的行为应该与子程序直接运行时的行为相同。如果一个问题在child直接运行时正确输出,则应该在parent运行时正确输出。

标签: c console posix pipe


【解决方案1】:

似乎我需要获取流 0 和 1 的状态标志(使用 fcntl),并在管道端点上设置相同的标志,用于代替实际流 0 和 1。readline() PHP 的提示仍然没有出现,但这并不是说直到用户输入某些内容才会出现,而是它根本没有出现,这很奇怪。但是我的小程序与 ffmpeg 配合得很好(它会提示你是否要覆盖文件),所以我认为它对我的目的来说已经足够了。感谢所有回答的人。

【讨论】:

  • 有些程序会在stderr而不是stdout上显示交互式提示。检查一下。
【解决方案2】:

现在,当我看到您的整个代码时,它存在一些问题。

第一个(并不是真正的问题)是您不必在调用dup2 之前关闭描述符,因为dup2 会为您关闭它们。

第二,你真的不需要在分叉之前进行任何描述符复制。你可以在分叉之后做所有这些。说到这一点,当您对标准进行第一次进出复制时,不需要这样做,因为在父流程中,您可以使用这些副本重置标准进出恢复到原始状态。

您也不会检查某些函数的错误(dupdup2fork 等)。这包括处理EWOULDBLOCK 之类的事情,比如read 通常会返回。

您还两次在infd[0] 上设置O_NONBLOCK 标志,一次在pipe2 调用中,另一次在父级中手动设置。

也不需要将fsync 与管道和标准输出一起使用。它主要用于在慢速设备上打开的文件 s 系统缓存数据以加快写入调用的速度。虽然控制台输出可以被认为是一个慢速设备,但它仍然没有像磁盘写入那样被缓存,并且使用文件描述符写入标准输出是无缓冲的。管道是缓冲的,但仍然没有什么要同步的,因为缓冲区只是为了让一个进程可以在一端写入,而另一个进程可以在另一端读取。

现在真正的问题可能是:控制台。为了能够处理编辑、光标移动等事情,终端仿真器应用程序会缓冲输入,并且在用户按下 Enter 之前不会将其提供给您的进程kbd> 键。您必须使用 termios 功能来禁用它。有关如何禁用 CANONICAL 模式(禁用行缓冲模式)的非常简单的示例,请参见 for example here


至于您的程序,编写像您这样的程序的典型方式更像是这样:

int pipes1[2], pipes2[2];

pipe(pipes1);
pipe(pipes2);

int res = fork();
if (res == -1)
    perror("fork");
else if (res == 0)
{
    dup2(pipes1[0], STDIN_FILENO);
    dup2(pipes2[1], STDOUT_FILENO);

    close(pipes1[1]);
    close(pipes2[0]);

    exec(...);
}
else
{
    close(pipes1[0]);
    close(pipes2[1]);

    /* Read from `pipes2[0]`, write to `pipes1[1]` */
}

我认为代码更少更简单。

【讨论】:

  • 当我直接运行php脚本时,我立即看到了question。但是当我作为 C 程序的子程序运行它时,question 不会出现。这是真正的问题。此外,我不在乎 child 是否阻塞,直到它得到一个完整的行。孩子可以阻止它想要的一切。只要父母在输入字符可用时立即将其发送给孩子。
  • @user2634914 现在,当我看到您的代码时,很容易弄清楚您在做什么,以及问题所在。我完全重写了我的答案。
【解决方案3】:

无法正常工作的原因是stdinstdoutstderr 不能表征子程序的整个输入和输出。相反,我们对附加到子进程的 TTY 更感兴趣。实现这类程序的正确方法是使用forkpty()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-02-01
    • 1970-01-01
    • 2021-12-24
    • 2020-11-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    相关资源
    最近更新 更多