You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用安全的Rust实现两个进程的标准输入/输出双向对接?

如何用安全的Rust实现两个进程的标准输入/输出双向对接?

嘿,你想要的是两个进程之间的双向管道通信对吧?我看了你现有的单向实现,确实是Rust里处理进程管道的常规操作,但双向的话得注意几个关键点——比如避免死锁、正确管理文件描述符,还要用完全安全的写法。

首先得明白:双向通信需要两对独立的管道,分别负责两个方向的数据流动:

  1. 第一对管道:把进程B的输出接到进程A的输入
  2. 第二对管道:把进程A的输出接到进程B的输入

如果直接像单向那样硬连,不仅会遇到所有权冲突,还很容易触发死锁——两个进程都卡在等待对方输入的状态,谁也没法继续。

方案1:用标准库+线程的安全实现(Unix平台)

这个方案只用到Rust标准库,唯一的unsafe是封装了底层的管道创建逻辑,但对外暴露的是完全安全的API:

use std::error::Error;
use std::io;
use std::os::unix::io::{FromRawFd, OwnedFd};
use std::process::{Command, Stdio};
use std::thread;

fn main() -> Result<(), Box<dyn Error>> {
    // 创建两对管道,分别处理双向数据流
    // 管道1:进程B的输出 → 进程A的输入
    let (pipe1_read, pipe1_write) = create_pipe()?;
    // 管道2:进程A的输出 → 进程B的输入
    let (pipe2_read, pipe2_write) = create_pipe()?;

    // 启动进程A:从pipe1读输入,向pipe2写输出
    let mut child_a = Command::new("rev")
        .stdin(Stdio::from(pipe1_read))
        .stdout(Stdio::from(pipe2_write))
        .spawn()?;

    // 启动进程B:从pipe2读输入,向pipe1写输出
    let mut child_b = Command::new("tr")
        .arg("a-z")
        .arg("A-Z")
        .stdin(Stdio::from(pipe2_read))
        .stdout(Stdio::from(pipe1_write))
        .spawn()?;

    // 父进程必须关闭不再使用的管道端,否则子进程会一直等待管道关闭,无法正常退出
    drop(pipe1_write);
    drop(pipe2_read);

    // 用线程同时等待两个进程结束,避免死锁(如果顺序等待,可能一个进程卡住另一个也无法推进)
    let handle_a = thread::spawn(move || child_a.wait());
    let handle_b = thread::spawn(move || child_b.wait());

    // 等待线程完成并处理结果
    handle_a.join()??;
    handle_b.join()??;

    Ok(())
}

// 安全封装管道创建逻辑,返回读端和写端的OwnedFd(自动管理生命周期)
fn create_pipe() -> Result<(OwnedFd, OwnedFd), io::Error> {
    let mut fds = [0; 2];
    unsafe {
        // 这里的unsafe仅用于调用libc的pipe函数,我们确保返回的文件描述符被正确封装为OwnedFd
        if libc::pipe(fds.as_mut_ptr()) == -1 {
            return Err(io::Error::last_os_error());
        }
        Ok((OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])))
    }
}

方案2:用异步库(Tokio)的无unsafe实现(跨平台)

如果想要完全避免unsafe,同时还能不用额外的手动线程,Tokio的异步进程和IO工具是更好的选择——它封装了所有底层细节,提供跨平台的安全API:

use tokio::process::Command;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 创建双向管道对(Tokio提供的duplex方法自动处理底层管道)
    let (pipe_a_in, mut pipe_b_out) = tokio::io::duplex(1024);
    let (pipe_b_in, mut pipe_a_out) = tokio::io::duplex(1024);

    // 启动进程A:输入来自pipe_a_in,输出写入pipe_a_out
    let mut child_a = Command::new("rev")
        .stdin(pipe_a_in)
        .stdout(pipe_a_out)
        .spawn()?;

    // 启动进程B:输入来自pipe_b_in,输出写入pipe_b_out
    let mut child_b = Command::new("tr")
        .arg("a-z")
        .arg("A-Z")
        .stdin(pipe_b_in)
        .stdout(pipe_b_out)
        .spawn()?;

    // 异步等待两个进程完成,无需手动线程,也不会死锁
    let status_a = child_a.wait().await?;
    let status_b = child_b.wait().await?;

    println!("进程A退出状态:{}", status_a);
    println!("进程B退出状态:{}", status_b);

    Ok(())
}

关键注意事项

  1. 关闭冗余管道端:父进程必须关闭不需要的管道句柄,否则子进程会一直认为管道还有活跃的写入端,不会退出。
  2. 避免死锁:同步代码里必须用线程同时等待两个进程;异步代码则由runtime自动调度,避免了死锁问题。
  3. 所有权管理:Rust的OwnedFd(或Tokio的异步管道)会自动管理文件描述符的生命周期,不用担心资源泄漏。

备注:内容来源于stack exchange,提问作者Timmmm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:39:29