Process 如何读取子进程的输出而不被阻塞?

Process 如何读取子进程的输出而不被阻塞?,process,io,rust,blocking,pty,Process,Io,Rust,Blocking,Pty,我正在Rust中制作一个小的ncurses应用程序,它需要与子进程进行通信。我已经有了一个用CommonLisp编写的原型。我正试图重写它,因为CL为这么一个小的工具使用了大量的内存 我在弄清楚如何与子流程交互时遇到了一些问题 我目前所做的大致如下: 创建流程: let mut program = match Command::new(command) .args(arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped

我正在Rust中制作一个小的ncurses应用程序,它需要与子进程进行通信。我已经有了一个用CommonLisp编写的原型。我正试图重写它,因为CL为这么一个小的工具使用了大量的内存

我在弄清楚如何与子流程交互时遇到了一些问题

我目前所做的大致如下:

  • 创建流程:

    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;
        }
    };
    
  • 将其传递给无限(直到用户退出)循环,该循环读取和处理输入,并侦听如下输出(并将其写入屏幕):

  • 但是,调用
    read_to_string
    会阻塞程序,直到进程退出。从我所看到的
    read\u to\u end
    read
    似乎也会阻塞。如果我尝试运行像立即退出的
    ls
    之类的东西,它会工作,但是对于像
    python
    sbcl
    之类不退出的东西,它只会在我手动终止子进程后继续

    基于,我将代码更改为使用
    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
    设置超时的方法。

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

    操作系统渴望将数据返回到读取过程中,因此,如果您只想等待下一行数据,并在数据到达后立即处理数据,那么Shepmaster在中(以及在他的回答中)建议的方法是有效的。
    虽然理论上它不必工作,因为允许操作系统让BufReader在
    read
    中等待更多数据,但实际上操作系统更喜欢早期的“短读”而不是等待

    当您需要处理多个流(如子进程的
    stdout
    stderr
    )或多个进程时,这种基于
    BufReader
    的简单方法变得更加危险。例如,当子进程等待您排空其
    stderr
    管道,而您的进程在等待其空
    stdout
    时被阻止时,基于
    BufReader
    的方法可能会死锁

    类似地,如果不希望程序无限期地等待子进程,则不能使用
    BufReader
    。可能您希望在孩子仍在工作且没有输出时显示进度条或计时器

    如果您的操作系统恰好不急于将数据返回到进程(更喜欢“完全读取”而不是“短读取”),则不能使用基于
    BufReader
    的方法,因为在这种情况下,子进程打印的最后几行可能会进入灰色区域:操作系统得到了它们,但它们不够大,无法填满
    BufReader
    的缓冲区

    BufReader
    仅限于
    Read
    接口允许它对流执行的操作,它的阻塞程度不亚于底层流。为了提高效率,它会将输入分块处理,告诉操作系统尽可能多地填充可用的缓冲区

    您可能想知道为什么在这里读取数据块如此重要,为什么
    BufReader
    不能一个字节一个字节地读取数据。问题在于,要从流中读取数据,我们需要操作系统的帮助。另一方面,我们不是操作系统,我们与它隔离工作,以便在流程出现问题时不会弄乱它。因此,为了调用操作系统,需要转换到“内核模式”,这也可能导致“上下文切换”。这就是为什么调用操作系统读取每个字节的成本很高。我们需要尽可能少的操作系统调用,因此我们可以批量获得流数据

    要在不阻塞的情况下等待流,您需要一个非阻塞流。米奥,很可能是带着,但我还没查过

    流的非阻塞特性应该使读取数据块成为可能,而不管操作系统是否喜欢“短读”。因为非阻塞流从不阻塞。如果流中没有数据,它会简单地告诉您

    在没有非阻塞流的情况下,您将不得不求助于生成线程,以便阻塞读取将在单独的线程中执行,从而不会阻塞主线程。您可能还希望逐字节读取流,以便在操作系统不喜欢“短读”时立即对行分隔符作出反应。下面是一个工作示例:

    注意:下面是一个函数示例,该函数允许通过共享的字节向量监视程序的标准输出:

    use std::io::Read;
    use std::process::{Command, Stdio};
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    /// Pipe streams are blocking, we need separate threads to monitor them without blocking the primary thread.
    fn child_stream_to_vec<R>(mut stream: R) -> Arc<Mutex<Vec<u8>>>
    where
        R: Read + Send + 'static,
    {
        let out = Arc::new(Mutex::new(Vec::new()));
        let vec = out.clone();
        thread::Builder::new()
            .name("child_stream_to_vec".into())
            .spawn(move || loop {
                let mut buf = [0];
                match stream.read(&mut buf) {
                    Err(err) => {
                        println!("{}] Error reading from stream: {}", line!(), err);
                        break;
                    }
                    Ok(got) => {
                        if got == 0 {
                            break;
                        } else if got == 1 {
                            vec.lock().expect("!lock").push(buf[0])
                        } else {
                            println!("{}] Unexpected number of bytes: {}", line!(), got);
                            break;
                        }
                    }
                }
            })
            .expect("!thread");
        out
    }
    
    fn main() {
        let mut cat = Command::new("cat")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .expect("!cat");
    
        let out = child_stream_to_vec(cat.stdout.take().expect("!stdout"));
        let err = child_stream_to_vec(cat.stderr.take().expect("!stderr"));
        let mut stdin = match cat.stdin.take() {
            Some(stdin) => stdin,
            None => panic!("!stdin"),
        };
    }
    
    注意,异步std中调用时的
    wait
    也是阻塞的。它只是阻止了一个系统线程,而只是阻止了一个未来链(本质上是一个无堆栈的绿色线程)。接口是非阻塞接口。在中,我询问了开发人员这些API是否有短读保证。

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

    操作系统渴望将数据返回到读取过程中,因此如果您只想等待下一行并在它一进来就处理它,那么方法会给出建议
    use std::io::Read;
    use std::process::{Command, Stdio};
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    /// Pipe streams are blocking, we need separate threads to monitor them without blocking the primary thread.
    fn child_stream_to_vec<R>(mut stream: R) -> Arc<Mutex<Vec<u8>>>
    where
        R: Read + Send + 'static,
    {
        let out = Arc::new(Mutex::new(Vec::new()));
        let vec = out.clone();
        thread::Builder::new()
            .name("child_stream_to_vec".into())
            .spawn(move || loop {
                let mut buf = [0];
                match stream.read(&mut buf) {
                    Err(err) => {
                        println!("{}] Error reading from stream: {}", line!(), err);
                        break;
                    }
                    Ok(got) => {
                        if got == 0 {
                            break;
                        } else if got == 1 {
                            vec.lock().expect("!lock").push(buf[0])
                        } else {
                            println!("{}] Unexpected number of bytes: {}", line!(), got);
                            break;
                        }
                    }
                }
            })
            .expect("!thread");
        out
    }
    
    fn main() {
        let mut cat = Command::new("cat")
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()
            .expect("!cat");
    
        let out = child_stream_to_vec(cat.stdout.take().expect("!stdout"));
        let err = child_stream_to_vec(cat.stderr.take().expect("!stderr"));
        let mut stdin = match cat.stdin.take() {
            Some(stdin) => stdin,
            None => panic!("!stdin"),
        };
    }
    
    try_s! (stdin.write_all (b"echo hello world\n"));
    try_s! (wait_forˢ (&out, 0.1, 9., |s| s == "hello world\n"));
    
    use std::process::Stdio;
    use futures::StreamExt; // 0.3.1
    use tokio::{io::BufReader, prelude::*, process::Command}; // 0.2.4, features = ["full"]
    
    #[tokio::main]
    async fn main() {
        let mut cmd = Command::new("/tmp/slow.bash")
            .stdout(Stdio::piped()) // Can do the same for stderr
            .spawn()
            .expect("cannot spawn");
    
        let stdout = cmd.stdout().take().expect("no stdout");
        // Can do the same for stderr
    
        // To print out each line
        // BufReader::new(stdout)
        //     .lines()
        //     .for_each(|s| async move { println!("> {:?}", s) })
        //     .await;
    
        // To print out each line *and* collect it all into a Vec
        let result: Vec<_> = BufReader::new(stdout)
            .lines()
            .inspect(|s| println!("> {:?}", s))
            .collect()
            .await;
    
        println!("All the lines: {:?}", result);
    }
    
    use std::process::{Command, Stdio};
    use tokio::{prelude::*, runtime::Runtime}; // 0.1.18
    use tokio_threadpool; // 0.1.13
    
    fn stream_command_output(
        mut command: Command,
    ) -> impl Stream<Item = Vec<u8>, Error = tokio_threadpool::BlockingError> {
        // Ensure that the output is available to read from and start the process
        let mut child = command
            .stdout(Stdio::piped())
            .spawn()
            .expect("cannot spawn");
        let mut stdout = child.stdout.take().expect("no stdout");
    
        // Create a stream of data
        stream::poll_fn(move || {
            // Perform blocking IO
            tokio_threadpool::blocking(|| {
                // Allocate some space to store anything read
                let mut data = vec![0; 128];
                // Read 1-128 bytes of data
                let n_bytes_read = stdout.read(&mut data).expect("cannot read");
    
                if n_bytes_read == 0 {
                    // Stdout is done
                    None
                } else {
                    // Only return as many bytes as we read
                    data.truncate(n_bytes_read);
                    Some(data)
                }
            })
        })
    }
    
    fn main() {
        let output_stream = stream_command_output(Command::new("/tmp/slow.bash"));
    
        let mut runtime = Runtime::new().expect("Unable to start the runtime");
    
        let result = runtime.block_on({
            output_stream
                .map(|d| String::from_utf8(d).expect("Not UTF-8"))
                .fold(Vec::new(), |mut v, s| {
                    print!("> {}", s);
                    v.push(s);
                    Ok(v)
                })
        });
    
        println!("All the lines: {:?}", result);
    }