Search code examples
cprocesspipeforkexec

Closing the write-end of a pipe doesn't send an EOF to the other process


I'm trying to implement a minishell which must be able to pipe commands.

Initially, I was executing the processes sequentially, that is, I was waiting for nth a process to terminate before starting the n + 1th process (this is a minimal reproducible example which executes ls | wc -l):

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

pid_t   family[2];
int     pipefd[2];
extern char **environ;

void    ft_exec(int i)
{
    char **ls = malloc(sizeof(char *) * 2);
    ls[0] = "/bin/ls";
    ls[1] = NULL;

    char **wc = malloc(sizeof(char *) * 3);
    wc[0] = "/usr/bin/wc";
    wc[1] = "-l";
    wc[2] = NULL;

    printf("ENTERED BY %d\n", getpid());
    if (i == 0)
    {
        dup2(pipefd[1], 1);
        execve(ls[0], ls, environ);
    }
    if (i == 1)
    {
        dup2(pipefd[0], 0);
        execve(wc[0], wc, environ);
    }
}

void    ft_interpret(void)
{
    int i;

    i = 0;
    while (i < 2)
    {
        family[i] = fork();
        printf("CREATED %d INSIDE %d\n", family[i], getpid());
        if (family[i] == 0)
            ft_exec(i);
        waitpid(family[i], NULL, 0);
        printf("TERMINATED %d IN %d\n", family[i], getpid());
        if (i == 0)
        {
            printf("CLOSED W%d\n", pipefd[1]);
            close(pipefd[1]);
        }
        else if (i == 1)
        {
            printf("CLOSED R%d\n", pipefd[0]);
            close(pipefd[0]);
        }
        i++;
    }
}

int main(int argc, char **argv)
{
    (void)argc;
    (void)argv;

    pipe(pipefd);
    printf("PARENT ID IS %d\n", getpid());
    printf("CREATED PIPE WITH FDS R%d AND W%d\n", pipefd[0], pipefd[1]);
    ft_interpret();
}

However, I realized that this is not how the shell works, and in case if a command like cat /dev/urandom | head -c 100 is given, the program will be stuck in a deadlock.

So I decided to fork the processes first, then whichever one of them exits, I close the associated write-ends of pipes.

For some reason, the reading command (wc in this case; if we change arguments to non-reading command, such as printenv, it'll execute correctly) gets stuck on reading, even though the main process has closed the write-end of the pipe and must've sent an EOF to wc.

Here's the modified code:

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

pid_t   family[2];
int     pipefd[2];
extern char **environ;

void    ft_exec(int i)
{
    char **ls = malloc(sizeof(char *) * 2);
    ls[0] = "/bin/ls";
    ls[1] = NULL;

    char **wc = malloc(sizeof(char *) * 3);
    wc[0] = "/usr/bin/wc";
    wc[1] = "-l";
    wc[2] = NULL;

    printf("ENTERED BY %d\n", getpid());
    if (i == 0)
    {
        dup2(pipefd[1], 1);
        execve(ls[0], ls, environ);
    }
    if (i == 1)
    {
        dup2(pipefd[0], 0);
        execve(wc[0], wc, environ);
    }
}

void    ft_block_main_process(void)
{
    int     i;
    pid_t   terminated;

    i = 0;
    while (i < 2)
    {
        terminated = waitpid(-1, NULL, 0);
        printf("TERMINATED %d IN %d\n", terminated, getpid());
        if (terminated == family[0])
        {
            printf("CLOSED W%d\n", pipefd[1]);
            close(pipefd[1]);
        }
        else if (terminated == family[1])
        {
            printf("CLOSED R%d\n", pipefd[0]);
            close(pipefd[0]);
        }
        i++;
    }
}

void    ft_interpret(void)
{
    int i;

    i = 0;
    while (i < 2)
    {
        family[i] = fork();
        printf("CREATED %d INSIDE %d\n", family[i], getpid());
        if (family[i] == 0)
            ft_exec(i);
        i++;
    }
    ft_block_main_process();
}

int main(int argc, char **argv)
{
    (void)argc;
    (void)argv;

    pipe(pipefd);
    printf("PARENT ID IS %d\n", getpid());
    printf("CREATED PIPE WITH FDS R%d AND W%d\n", pipefd[0], pipefd[1]);
    ft_interpret();
}

Why does this happen?


Solution

  • Thanks for the insightful comments.

    If I add close(pipefd[0]) and close(pipefd[1]) immediately after dup2()s, the issue gets solved.

    I thought that it was enough to close the pipe only from the main, shell process, but it turned out that all occurrences of fds have to be closed, including the ones inside children who inherit a copy.

    The reason I got confused is the fact that the first piece of code worked. Now I realize that there, I was waiting for the first (ls) process to exit, so when it exits, its copy of the write-end gets destroyed, and I close the last copy of it in the main process. Then wc doesn't inherit any.

    So now in my shell, during execution inside a child, after dup2()ing the descriptors, I close each and every descriptor created by pipe(), and it works.