PAYATHON 2026

Rust 中如何安全地双向连接两个子进程的标准流?

数据小陈

可以用安全 Rust 实现双向转发。对于通用的双向数据流,需要同时处理两个方向。使用 std::process 中的 Stdio::piped() 可以在子进程和父进程之间建立管道;再用两个线程分别调用 io::copy,将两个子进程的输出送入对方的输入。

为什么需要并发转发

题目中的代码把 child1.stdout 接到了 child2.stdin,只建立了单向管道。要增加反向传输,还需要同时处理这两条数据流:

  • child1.stdout → child2.stdin
  • child2.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 没有直接将两个已创建子进程的输出句柄互相接成循环管道的接口。

备注:内容仅供参考。