首页 文章

如何在不阻塞Rust的情况下读取子进程的输出?

提问于
浏览
9

我正在Rust中创建一个小的ncurses应用程序,需要与子进程通信 . 我已经有了一个用Common Lisp编写的原型; gif here希望能展示我想做的事情 . 我正在尝试重写它,因为CL为这么小的工具使用了大量的内存 .

我之前没有使用过Rust(或其他低级语言),而且我在弄清楚如何与子进程交互时遇到了一些麻烦 .

我目前正在做的大致是这样的:

  • 创建流程:
let mut program = match Command::new(command)
    .args(arguments)
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn() {
        Ok(child) => child,
        Err(_) => {
            println!("Cannot run program '{}'.", command);
            return;
        },
    };
  • 将其传递给无限(直到用户退出)循环,该循环读取并处理输入并像这样监听输出(并将其写入屏幕):
fn listen_for_output(program: &mut Child, 
                     output_viewer: &TextViewer) {
    match program.stdout {
        Some(ref mut out) => {
            let mut buf_string = String::new();
            match out.read_to_string(&mut buf_string) {
                Ok(_) => output_viewer.append_string(buf_string),
                Err(_) => return,
            };
        },
        None => return,
    };
}

然而,对 read_to_string 的调用会阻止程序直到进程退出 . 从我所看到的 read_to_endread 似乎也阻止了 . 如果我尝试运行类似 ls 的东西,它会立即退出,它会起作用,但是有些东西不会像 python 或_2858223那样退出,只有在我手动杀死子进程后它才会继续 .

Edit:

基于this answer,我将代码更改为使用 BufReader

fn listen_for_output(program: &mut Child, 
                     output_viewer: &TextViewer) {
    match program.stdout.as_mut() {
        Some(out) => {
            let buf_reader = BufReader::new(out);
            for line in buf_reader.lines() {
                match line {
                    Ok(l) => {
                        output_viewer.append_string(l);
                    },
                    Err(_) => return,
                };
            }
        },
        None => return,
    }
}

但问题仍然存在 . 它将读取所有可用的行,然后阻止 . 由于该工具应该适用于任何程序,因此在尝试读取之前无法猜出输出何时结束 . 似乎没有办法为 BufReader 设置超时 .

1 回答

  • 11

    默认情况下,流是阻止的 . TCP / IP流,文件系统流,管道流,它们都是阻塞的 . 当你告诉一个流给你一大块字节时,它会停止并等到它有给定的字节数或者发生其他事情(一个interrupt,一个流的结束,一个错误) .

    操作系统急于将数据返回到读取过程,因此如果你想要的只是等待下一行并在它进入时立即处理它,那么Shepmaster在Unable to pipe to or from spawned child process more than once中建议的方法就可以了 . (理论上它没有必要,因为允许操作系统使 BufReader 等待 read 中的更多数据,但实际上操作系统更喜欢早期"short reads"等待) .

    当您需要处理多个流(如子进程的 stdoutstderr )或多个进程时,这种简单的基于_2858234的方法会停止工作 . 例如,基于 BufReader 的方法可能会在子进程等待您耗尽 stderr 管道而您的进程被阻塞等待它的空 stdout 时死锁 .

    同样,如果您不希望程序无限期地等待子进程,则不能使用 BufReader . 也许您想要在孩子仍在工作时显示进度条或计时器并且没有输出 .

    如果您的操作系统没有急于将数据返回到进程(更喜欢"full reads"到"short reads"),则不能使用基于 BufReader 的方法,因为在这种情况下,子进程打印的最后几行可能会以灰色结束zone:操作系统得到它们,但它们不够大,无法填充 BufReader 的缓冲区 .

    BufReader 仅限于 Read 接口允许它对流执行的操作,它的阻塞程度不低于底层流 . 为了提高效率,它将以块的形式输入,告诉操作系统填充尽可能多的缓冲区 .

    您可能想知道为什么在块中读取数据如此重要,为什么 BufReader 不能逐字节地读取数据 . 问题是要从流中读取数据,我们需要操作系统的帮助 . 另一方面,我们不是操作系统,我们与它隔离工作,以免在我们的流程出现问题时弄乱它 . 因此,为了调用操作系统,需要转换到"kernel mode",这也可能会导致"context switch" . 这就是调用操作系统读取每个字节的原因很昂贵 . 我们希望尽可能少的OS调用,因此我们批量获取流数据 .

    要在没有阻止的情况下等待流,您需要一个非阻塞流 . MIO promises to have the required non-blocking stream support for pipes,很可能是PipeReader,但到目前为止我还没有检查过它 .

    流的非阻塞性质应该能够以块的形式读取数据,而不管操作系统是否更喜欢“短读取” . 因为非阻塞流永远不会阻塞 . 如果没有数据它简单地告诉你的流 .

    在没有阻塞流的情况下,你阻止你的主要线程 . 您可能还希望逐字节读取流,以便在操作系统不喜欢"short reads"的情况下立即对行分隔符做出反应 . 这是一个有效的例子:https://gist.github.com/ArtemGr/db40ae04b431a95f2b78 .

相关问题