Process 如何读取子进程的输出而不被阻塞?
我正在Rust中制作一个小的ncurses应用程序,它需要与子进程进行通信。我已经有了一个用CommonLisp编写的原型。我正试图重写它,因为CL为这么一个小的工具使用了大量的内存 我在弄清楚如何与子流程交互时遇到了一些问题 我目前所做的大致如下: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
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);
}