0

Here's my attempt to try to understand how to do correct piping between two child processes. I'm simply trying to pass the output of one Linux command to another (ls to cat) and have the program return successfully. However, I'm guessing that the second child that is forked is getting stuck and the parent is forever waiting on this child. I have been fiddling with this code for a long time trying to find out why it's getting stuck. I'm kind of a noob when it comes to C system programming, but I am trying to learn.

Does anybody have any idea why the program does not exit, but hangs on cat?

Any help would be greatly appreciated.

Thanks.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    char *a[2] = {"/bin/ls", NULL};
    char *b[2] = {"/bin/cat", NULL};
    char *envp[2] = {getenv("PATH"), NULL};
    int fd[2], status;
    pipe(fd);
    int old_std_out = dup(1);
    int old_std_in = dup(0);
    dup2(fd[1], 1);
    int pid = fork();
    switch(pid)
    {
        case -1:
            perror("Forkscrew");
            exit(1);
            break;
        case 0:
            execve(a[0], a, envp);
            exit(0);
            break;
       default:
            waitpid(-1, &status, 0);
            dup2(old_std_out, 1);
            break;
    }
    dup2(fd[0], 0);
    pid = fork();
    switch(pid)
    {
        case -1:
            perror("Forkscrew");
            exit(1);
            break;
        case 0:
            execve(b[0], b, envp);
            exit(0);
            break;
        default:
            waitpid(-1, &status, 0);
            dup2(old_std_in, 0);
            break;
    }
    printf("\n");
    return 0;
}
user3499524
  • 173
  • 9
  • Aren't you execing two cats? You fork and then both of those fork again? – Sami Kuhmonen May 17 '17 at 04:34
  • What do you think will happen if `ls`s output doesn't fit a pipe-buffer? – EOF May 17 '17 at 04:39
  • @SamiKuhmonen Execing two cats? Maybe? The first one does exec on array a, and the next exec on array b. I thought fork() returns child and parent. – user3499524 May 17 '17 at 04:48
  • @EOF I'm not sure what would happen. If I remove the last waitpid statement it works just fine, but cat is still running as a zombie. – user3499524 May 17 '17 at 04:49
  • @user3499524: If `ls`s output doesn't fit a pipe buffer, `ls` will *block* when the buffer is full. Meanwhile, your parent process is blocked on `waitpid()` *before* it creates the `cat`-process, so before any `read()` from the pipe. Each of your processes is waiting for the other, so you are *deadlocked*. – EOF May 17 '17 at 04:51
  • @EOF That might be the problem... But cat does execute and prints out the ls. it prints out all the contents of the directory, I've verified this. Plus, there's only about around 15 files. After cat is done (or I should say, all directory contents are printed), however, it just hangs there. Cat never comes back is the issue I think. – user3499524 May 17 '17 at 04:55
  • 2
    A more subtle deadlock (but one that happens *always*, not just when a pipe buffer is full) is associated with the read-end of the pipe. When `ls` terminates, it closes its copies of the write-end of the pipe. However, both your parent process *and* your `cat`-process still have a copy of the write end (in fact, the `printf()` at the end of your parent process writes to it...), so a `read()` from the pipe in `cat` will block until the write end is closed, which never happens because `cat` doesn't even know about the filedescriptor of the write end, and the parent doesn't terminate. – EOF May 17 '17 at 04:57
  • 1
    @SamiKuhmonen `exec()` doesn't return if successful. – EOF May 17 '17 at 04:57
  • @EOF OK, then not a problem – Sami Kuhmonen May 17 '17 at 04:58
  • @EOF I'll be a little honest, I might not know exactly what you mean. You say "close its copy to the write-end of the pipe". Doesn't dup2() close the current one you're duping the new file descriptor onto? In otherwords, in the parent of the first exec, isn't dup2() closing the pipe when I dup back to stdout? – user3499524 May 17 '17 at 05:09
  • 1
    @user3499524: Ah, right, I didn't see you `dup2(old_std_out, 1);`ing back. It's not the usual way to do this kind of thing, you usually should only redirect the filedescriptors *after* `fork()`, then you wouldn't need this. Since you `dup2(fd[1], 1);` *before* the first `fork()`, you keep around two copies of the write-end of the pipe. One, you close implicitly by `dup2(old_std_out, 1);`, the other stays. – EOF May 17 '17 at 05:15
  • Would you mind elaborating on that a little more? For instance, I can dup2() after fork() inside the switch statement, or after the switch statement before the next fork(), or literally right after the fork() command. Which after the fork() are you referring to? It may fix my issue :) – user3499524 May 17 '17 at 05:21
  • One thing I know is, if I dup2() in the child after the fork, then the next child wont know the pipe address, will it? – user3499524 May 17 '17 at 05:22
  • 1
    A comprehensive answer should probably not be made in a comment. Unfortunately, I don't have time right now, I might in a few hours. Anyway, for a quick fix of the second deadlock, add `close(fd[1])` before the second `fork()`, see it that helps. – EOF May 17 '17 at 05:27
  • @EOF Genius sir. This seems to have fixed my issue. I suppose dup2() wasn't closing it then? Interesting. Well, if you post it, I will give you the solution points guaranteed because it fixed my problem. Thanks. – user3499524 May 17 '17 at 05:32

1 Answers1

2

There are two potential deadlocks in your program.

First, the first child (ls) may block when trying to write to the pipe, in which case waitpid() will not return until ls terminates, and ls will not terminate until the second child (cat) starts executing, which can't happen until waitpid() returns. => Deadlock.

Second, cat will read from its stdin until all file descriptors for the write end are closed. Both the parent process and cat have a copy of the write end, cat has it without knowing about it explicitly. Some operating systems will have read() not block if the only copy of the write end is in the same process (to avoid this deadlock), but this is not guaranteed. Either way, since the parent process keeps around a copy of the filedescriptor, and the parent process waitpid()s for the child, which waits for the write-end of the pipe to close, you have deadlock again.

Often, simplifying the program solves such problems:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    char *a[2] = {"/bin/ls", NULL};
    char *b[2] = {"/bin/cat", NULL};
    char *envp[2] = {getenv("PATH"), NULL};
    int fd[2], status;
    pipe(fd);
    //int old_std_out = dup(1); /*No need to copy stdout...*/
    //int old_std_in = dup(0);  /*...or stdin...*/
    //dup2(fd[1], 1);           /*...if you wait dup2()ing until you need to*/
    int pid = fork();
    switch(pid)
    {
        case -1:
            perror("Forkscrew");
            exit(1);
            //break; /*unreachable*/
        case 0:
            dup2(fd[1], STDOUT_FILENO); /*NOW we dup2()*/
            close(fd[0]); /*no need to pass these file descriptors to...*/
            close(fd[1]); /*...a program that doesn't expect to have them open*/ 
            execve(a[0], a, envp);
            exit(0); /*might want an error message*/
            //break; /*unreachable*/
       default:
            //waitpid(-1, &status, 0); /*don't wait yet*/
            //dup2(old_std_out, 1);
            close(fd[1]); /*we don't need this in the parent anymore*/
            break;
    }
    //dup2(fd[0], 0); /*not needed anymore*/
    pid = fork();
    switch(pid)
    {
        case -1:
            perror("Forkscrew");
            /*might want to ensure the first child can terminate*/
            exit(1);
            //break; /*unreachable*/
        case 0:
            dup2(fd[0], STDIN_FILENO);
            close(fd[0]); /*again, cat doesn't expect a fourth fd open*/
            execve(b[0], b, envp);
            /*again, error message would be nice*/
            exit(0);
            //break;
        default:
            //waitpid(-1, &status, 0);
            //dup2(old_std_in, 0);
            break;
    }
    waitpid(-1, &status, 0); /*don't wait until both children are created*/
    waitpid(-1, &status, 0);
    printf("\n");
    return 0;
}

As you see, I've left a number of suggestions for improvements, but this should already work fine now, provided the execve()s work alright.

EOF
  • 6,273
  • 2
  • 26
  • 50