c++pipeforkexecvpdup2

When implementing chained pipe command in C++, I failed to redirect input from pipe to STDIN_FILENO using `dup2` in the child process


I want to implement pipe command in shell, such as ls | cat | wc. Here's my pipe command implementation.

I use a pipe to store the results generated by every command. For every child process, it:

Here's my code:

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 << "$ ";

}

When I test the program by entering ls | cat. The last command successfully put the result of ls into the pipe. But I could not redirect input from pipe to STDIN_FILENO when executing the cat command.

The program just blocks when executing ls | cat. After debugging, I found that it's because the cat command is still waiting for user to input something, and the parent process is waiting for the child process to terminate.


Solution

  • I'm the one who raised this question. Now I've worked out the problem now and successfully implemented the multi-pipe program. I made 3 changes to the previous code:

    1. In the previous code, there's only one pipe. What I previously intended to do is achieve the process pipe -> STDIN -> execvp -> STDOUT -> pipe. But if I only use 1 pipe, the STDIN and STDOUT are connected. So In the current version of my program, I create a new pipe before fork() and close its write end, but I record its read end in a variable fd_in.
    2. The variable fd_in is set to 0 at first. Because some commands such as cat need to obtain input from the STDIN instead of pipe. After each command execution, the fd_in is updated to the current pipe's read end for the next command execution. While working with this problem, I found that pipe is not the same as local variable, which is pushed on the stack on process memory. It's stored in kernel memory rather than the process memory, so the created pipe won't go away immediately after the loop exits. But if we declares a local variable in a loop, it's gone after the loop exits. If we record the read end of the pipe, we could still reach it in the next loop.
    3. I use a variable count and compare it to the length of command vector in order to determine if it is the last command to execute. If it is not the last command, write the result to the pipe. Otherwise, just print out the result to the console. Therefore, there's no need to use the system call read to get the result out of pipe and store it in a buffer in the parent process. It's mainly for those command which print out results to the console immediately, such as cat. Besieds, it simplify the design.

    The new version of code as follows:

        while (getline(std::cin, s))
        {
            if (s == "exit")
            {
                return EXIT_SUCCESS;
            }
    
            int fd[2];
            int in_fd = 0; // input fd
    
            // split the command into multiple parts
            vector<string> tokens;
            boost::algorithm::split(tokens, s, boost::is_any_of("|"), boost::token_compress_on);
    
            int count = 1;
            int command_num = tokens.size();
    
            for (auto &command : tokens)
            {
                // initialize a pipe
                if (pipe(fd) == -1)
                {
                    perror("pipe creation failed!");
                    return EXIT_FAILURE;
                }
    
                // 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;
    
                    if (in_fd != 0)
                    {
                        // write the pipe value into stdin
                        dup2(in_fd, STDIN_FILENO);
                        close(in_fd);
                    }
    
                    if (count != command_num)
                    {
                        // 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);
    
                // close the current pipe write fd
                close(fd[1]);
                in_fd = fd[0];
                count += 1;
            }
    
            // // read out the pipe
            // char buffer[BUF_SIZ];
            // int count = read(in_fd, buffer, BUF_SIZ);
            // buffer[count] = '\0';
            // if (count > 0)
            // {
            //     fprintf(stdout, "%s", buffer);
            // }
            close(in_fd);
    
            cout << "$ ";
        }
    

    Thanks for Some programmmer dude's help. And I refer to an answer from this question in the process of solving this question. I hope my implementation will give you some inspirations.