Rust 中如何安全地双向连接两个子进程的标准流?
数据小陈
可以用安全 Rust 实现双向转发。对于通用的双向数据流,需要同时处理两个方向。使用 std::process 中的 Stdio::piped() 可以在子进程和父进程之间建立管道;再用两个线程分别调用 io::copy,将两个子进程的输出送入对方的输入。
为什么需要并发转发
题目中的代码把 child1.stdout 接到了 child2.stdin,只建立了单向管道。要增加反向传输,还需要同时处理这两条数据流:
child1.stdout→child2.stdinchild2.stdout→child1.stdin
如果在一个线程里依次复制,某个子进程可能先写满输出管道,等待对方读取;复制代码却在等待另一个方向的数据,双方就可能互相等待。下面的写法用两个线程分别持续转发两个方向的数据。
安全 Rust 示例
use std::io;
use std::process::{Child, Command, Stdio};
use std::thread;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut child1 = Command::new("program-a")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut child2 = Command::new("program-b")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let child1_out = child1.stdout.take().expect("child1 stdout 未设置为 piped");
let child1_in = child1.stdin.take().expect("child1 stdin 未设置为 piped");
let child2_out = child2.stdout.take().expect("child2 stdout 未设置为 piped");
let child2_in = child2.stdin.take().expect("child2 stdin 未设置为 piped");
let forward_1_to_2 = thread::spawn(move || {
let mut reader = child1_out;
let mut writer = child2_in;
io::copy(&mut reader, &mut writer)
});
let forward_2_to_1 = thread::spawn(move || {
let mut reader = child2_out;
let mut writer = child1_in;
io::copy(&mut reader, &mut writer)
});
let copy_1_to_2 = forward_1_to_2
.join()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "child1 到 child2 的转发线程异常退出"))?;
let copy_2_to_1 = forward_2_to_1
.join()
.map_err(|_| io::Error::new(io::ErrorKind::Other, "child2 到 child1 的转发线程异常退出"))?;
let status1 = child1.wait()?;
let status2 = child2.wait()?;
copy_1_to_2?;
copy_2_to_1?;
if !status1.success() || !status2.success() {
return Err(format!("子进程退出状态:child1={status1}, child2={status2}").into());
}
Ok(())
}
将 program-a 和 program-b 换成实际程序即可。示例通过 .take() 从 Child 中取出管道句柄,并将句柄移交给转发线程,不需要 unsafe。
注意事项
- 两个转发线程会持续复制数据,直到对应的输出端关闭。线程结束时,输入端句柄也会关闭,对端因此可以收到 EOF。
- 如果写入端提前关闭,
io::copy会返回错误。示例会在等待子进程退出后检查复制结果。 - 线程可以让两个方向的读取同时进行,但不会替应用协议决定何时结束。如果两个程序都要持续等待对方发送数据才能退出,就需要设计明确的结束信号或关闭时机。
- 题目原示例中的
ls通常不会读取 stdin,所以即使接上了管道,也不会处理反方向的数据。要验证双向通信,应使用会读写标准流的程序。 - 也可以用异步运行时、事件循环或特定平台的 I/O 接口并发处理两条流。标准库中的
Stdio没有直接将两个已创建子进程的输出句柄互相接成循环管道的接口。
备注:内容仅供参考。
