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

C++实现链式管道命令时dup2重定向STDIN_FILENO失败求助

链式Shell管道命令阻塞问题修复

问题场景

实现Shell风格的链式管道命令(如ls | cat | wc),要求子进程通过管道传递执行结果,重定向stdin/stdout后调用execvp执行命令。但测试ls | cat时程序阻塞,cat未读取管道输入,反而等待用户手动输入,dup2重定向未生效。

原错误代码

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#include <cerrno>
#include <boost/algorithm/string.hpp>

using namespace std;

#define BUF_SIZ 1024

int main() {
    string s;
    cout << "$ ";
    while(getline(std::cin, s)) {
        if(s == "exit") {
           return EXIT_SUCCESS;
        }

        // initialize a pipe
        int fd[2];
        if(pipe(fd) == -1) {
            perror("pipe creation failed!");
            return EXIT_FAILURE;
        }
        
        // split the command into multiple parts
        vector<string> tokens;
        boost::algorithm::split(tokens, s, boost::is_any_of("|"),boost::token_compress_on);
        for(auto& command: tokens) {
            // prepare to run the current command
        
            // get the current command
            boost::algorithm::trim(command);
            // split the command into an array of args
            vector<string> args;
            boost::algorithm::split(args,command,boost::is_any_of(" "),boost::token_compress_on);
            int argc = args.size();
            if(argc < 1) {
                cerr << "We need a command!" << endl;
                return EXIT_FAILURE; 
            }
        
            // run the current command
            pid_t child = fork();
            if(child == 0) {
                // setup the file name and input arguments
                const char* filename = args[0].c_str();
                char** argv = new char*[argc + 1];
                for(int i = 0; i < argc; i++) {
                    string args_str = args[i];
                    argv[i] = new char[10];
                    strcpy(argv[i],args_str.c_str());
                }
                argv[argc] = nullptr;
        
                // write the pipe value into stdin
                dup2(fd[0], STDIN_FILENO);
                close(fd[0]);
        
                // write stdout to the pipe
                dup2(fd[1], STDOUT_FILENO);
                close(fd[1]);
        
                // use execvp() to run the commmand
                execvp(filename,argv);
        
                // exec didn't work, so an error must have been occurred
                cerr << strerror(errno) << endl;
                delete[] argv;
                return EXIT_FAILURE;
            }
            
            // wait for the child process to complete
            int status;
            waitpid(child,&status,0);
        }
        // read out the pipe
        char* buffer = new char[BUF_SIZ];
        int count = read(fd[0],buffer,BUF_SIZ);
        if(count > 0) {
            fprintf(stdout, "%s", buffer);
        }
        
        delete buffer;        
        
        close(fd[0]);
        close(fd[1]);
        cout << "$ ";

    }
    return 0;
}

问题根源分析

  1. 单管道复用错误:所有命令共用同一个管道,导致ls的输出写入管道后,自己又尝试从同一个管道读,后续cat也无法正确获取前序命令的输出,数据流完全混乱。
  2. 进程等待时机错误:每次循环都立即waitpid等待当前子进程结束,此时管道的写端未关闭,后续进程读管道时会一直阻塞等待EOF。
  3. 管道FD管理混乱:父进程未及时关闭不需要的管道端,导致管道引用计数不为0,无法触发EOF信号。
  4. 参数内存分配错误:固定分配10字节存储参数,长参数会被截断,导致execvp执行失败。

修复后的代码

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>
#include <cerrno>
#include <boost/algorithm/string.hpp>

using namespace std;

#define BUF_SIZ 1024

int main() {
    string s;
    cout << "$ ";
    while(getline(cin, s)) {
        if(s == "exit") {
           return EXIT_SUCCESS;
        }

        vector<string> tokens;
        boost::algorithm::split(tokens, s, boost::is_any_of("|"), boost::token_compress_on);
        int prev_in = STDIN_FILENO; // 初始输入为标准输入

        for(size_t i = 0; i < tokens.size(); i++) {
            string& command = tokens[i];
            boost::algorithm::trim(command);
            vector<string> args;
            boost::algorithm::split(args, command, boost::is_any_of(" "), boost::token_compress_on);
            int argc = args.size();
            if(argc < 1) {
                cerr << "Invalid command!" << endl;
                break;
            }

            int fd[2];
            if(pipe(fd) == -1) {
                perror("pipe creation failed");
                return EXIT_FAILURE;
            }

            pid_t child = fork();
            if(child == 0) {
                // 重定向输入:如果不是第一个命令,用前一个管道的读端
                if(prev_in != STDIN_FILENO) {
                    dup2(prev_in, STDIN_FILENO);
                    close(prev_in);
                }

                // 重定向输出:如果不是最后一个命令,用当前管道的写端
                if(i != tokens.size() - 1) {
                    dup2(fd[1], STDOUT_FILENO);
                    close(fd[1]);
                }

                // 关闭当前管道的读端(子进程不需要)
                close(fd[0]);

                // 构建argv数组,按实际参数长度分配内存
                char** argv = new char*[argc + 1];
                for(int j = 0; j < argc; j++) {
                    const char* arg_str = args[j].c_str();
                    argv[j] = new char[strlen(arg_str) + 1];
                    strcpy(argv[j], arg_str);
                }
                argv[argc] = nullptr;

                execvp(argv[0], argv);

                // exec失败才会走到这里
                cerr << "Exec failed: " << strerror(errno) << endl;
                for(int j = 0; j < argc; j++) {
                    delete[] argv[j];
                }
                delete[] argv;
                _exit(EXIT_FAILURE);
            }

            // 父进程关闭不需要的管道端
            close(fd[1]);
            if(prev_in != STDIN_FILENO) {
                close(prev_in);
            }

            // 保存当前管道的读端,作为下一个命令的输入
            prev_in = fd[0];
        }

        // 读取最后一个命令的输出
        char buffer[BUF_SIZ];
        ssize_t count;
        while((count = read(prev_in, buffer, BUF_SIZ)) > 0) {
            write(STDOUT_FILENO, buffer, count);
        }
        close(prev_in);

        // 等待所有子进程结束
        while(wait(nullptr) > 0);

        cout << "\n$ ";
    }
    return 0;
}

修复要点说明

  • 管道链式传递:为每两个命令之间创建新管道,用prev_in保存前一个管道的读端,作为下一个命令的输入。
  • 正确重定向:第一个命令输入用标准输入,最后一个命令输出用标准输出,中间命令才重定向到管道。
  • 及时关闭FD:父进程和子进程都关闭不需要的管道端,确保管道能正确触发EOF。
  • 参数内存修复:按参数实际长度分配内存,避免截断。
  • 延迟进程等待:所有子进程启动后再统一等待,避免阻塞数据流传递。

内容的提问来源于stack exchange,提问作者Gwen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 14:39:59