Search code examples
clinuxpipefork

Linux got stuck using pipe() and dup2()


I am simulating the linux shell pipe operator, and I got stuck. The below is the minimal reproducable example of what I tried.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int do_command(char* args[])
{
    int status;
    pid_t pid = fork();

    if (pid == 0)
    {
        execvp(args[0], args);
    }
    else
    {
        wait(&status);
    }

    return 0;
}

int main() {
    int pipefd[2];
    pid_t pid;

    // Create a pipe
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // Redirect stdout to write end of the pipe
    if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    // the output will go to pipefd[1].
    char* echo[] = { "echo", "Hello, World!", NULL };
    do_command(echo);

    close(pipefd[1]);

    // Redirect stdin to read end of the pipe
    if (dup2(pipefd[0], STDIN_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    // since there is no argument to rev, it will automatically read from pipefd[0]
    char* rev[] = { "rev", NULL };
    do_command(rev);

    close(pipefd[0]);

    return 0;
}

After I run this, the program stopped without outputting any single word. I think there is something to do with close(), but I couldn't figure it out. I did close both pipes. How can I fix it?

And is there any way to fix it without changing the structure of do_command()? I want to simulate multiple pipe chain in the future, So I want the structure of function do_command remain same, in order to call the command easily.


Solution

  • As I noted in the comments, you have problems because you change the standard output of the controlling process to the write end of the pipe so that when the rev process is run, it is writing to the pipe. It's also reading from the pipe, which can't be healthy.

    You also need to launch child processes before waiting for either to finish.

    All in all, you need a major rewrite. Here's what I'd do:

    /* SO 7839-3593 */
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include "stderr.h"
    
    static void wait_loop(void)
    {
        int corpse;
        int status;
        while ((corpse = wait(&status)) > 0)
            err_remark("child %5d died with status 0x%.4X\n", corpse, status);
    }
    
    static void do_echo(int fd[2])
    {
        if (dup2(fd[1], STDOUT_FILENO) < 0)
            err_syserr("dup2() failed to duplicate fd %d to fd %d: ", fd[1], STDOUT_FILENO);
        close(fd[0]);
        close(fd[1]);
        char *echo[] = { "echo", "Hello, World!", NULL };
        err_remark("executing %s\n", echo[0]);
        execvp(echo[0], echo);
        err_syserr("failed to execute %s: ", echo[0]);
        /*NOTREACHED*/
    }
    
    static void do_rev(int fd[2])
    {
        if (dup2(fd[0], STDIN_FILENO) < 0)
            err_syserr("dup2() failed to duplicate fd %d to fd %d: ", fd[0], STDIN_FILENO);
        close(fd[0]);
        close(fd[1]);
        char *rev[] = { "rev", NULL };
        err_remark("executing %s\n", rev[0]);
        execvp(rev[0], rev);
        err_syserr("failed to execute %s: ", rev[0]);
        /*NOTREACHED*/
    }
    
    int main(void)
    {
        err_setarg0("so-7839-3593");
        err_setlogopts(ERR_PID|ERR_MILLI);
        int pipefd[2];
    
        // Create a pipe
        if (pipe(pipefd) == -1)
            err_syserr("pipe() failed: ");
    
        pid_t pid = fork();
        if (pid == -1)
            err_syserr("fork() failed: ");
        if (pid == 0)
            do_echo(pipefd);
    
        pid = fork();
        if (pid == -1)
            err_syserr("fork() failed: ");
        if (pid == 0)
            do_rev(pipefd);
    
        close(pipefd[0]);
        close(pipefd[1]);
    
        wait_loop();
    
        return 0;
    }
    

    This code uses my standard error reporting functions for succinctness. The code for them is available in my SOQ (Stack Overflow Questions) repository on GitHub as files stderr.c and stderr.h in the src/libsoq sub-directory. You could use the err(3) set of functions on Linux instead.

    The code in main() sets the program name and some logging options (always write the PID and the time to the nearest millisecond) before launching into the operational code. It creates the pipe and then forks the first child, which executes the function do_echo(). It then forks the second child, which executes the function do_rev(). It then closes both ends of the pipe (a very important step) and goes into a wait loop which reports each child as it exits. If the parent does not close the write end of the pipe before going into the wait loop, the rev process doesn't get EOF because the parent could (but won't) write to the pipe.

    The do_echo() function is executed by a child process. It duplicates the write end of the pipe to standard output (so the child writes to the pipe), then it closes both ends of the pipe and launches the echo command. If the execvp() returns, it failed, and the err_syserr() function reports the failure and exits.

    The do_rev() function is also executed by a child process. It duplicates the read end of the pipe to standard input (so the child reads from the pipe), then it too closes both ends of the pipe and launches the rev command, reporting failure and exiting if the launch fails.

    At one point when I ran the program, I had a bug testing the return value from dup2() and got the output:

    so-7839-3593: 2024-04-26 20:48:41.713 - pid=85728: dup2() failed to duplicate fd 4 to fd 1: error (22) Invalid argument
    so-7839-3593: 2024-04-26 20:48:41.714 - pid=85729: executing rev
    so-7839-3593: 2024-04-26 20:48:41.714 - pid=85727: child 85728 died with status 0x0100
    so-7839-3593: 2024-04-26 20:48:41.717 - pid=85727: child 85729 died with status 0x0000
    

    When I fixed the test, I got the output:

    so-7839-3593: 2024-04-26 20:51:48.298 - pid=85802: executing rev
    so-7839-3593: 2024-04-26 20:51:48.298 - pid=85801: executing echo
    !dlroW ,olleH
    so-7839-3593: 2024-04-26 20:51:48.302 - pid=85800: child 85801 died with status 0x0000
    so-7839-3593: 2024-04-26 20:51:48.302 - pid=85800: child 85802 died with status 0x0000
    

    When you're testing multi-process pipelines, make sure you have copious debug output properly tagged with the PID of the process generating it.

    If you want to generalize this, you have to work quite a bit harder. The first and last processes in a pipeline need special treatment. If you have three or more processes in the pipeline, the middle processes all follow a uniform pattern — there's an input pipe and an output pipe and you connect the read end of the input pipe to standard input, the write end of the output pipe to standard output, and close all four ends of the two pipes.