3个子进程只需要2个wait()

3 child processes require only 2 wait()

我正在尝试实现执行 ls| grep "pipes"| wc -l 的代码。为此,我创建了 3 个子进程并使用了 2 个管道。请查找使用的代码:

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

int main(void)
{
    //pid_t p;
    int pfds1[2],pfds2[2],s;
    pipe(pfds1);
    pipe(pfds2);
    

if(!fork()) //first child ls
{   
    //printf("ls ppid is:%d\n", getppid());
    dup2(pfds1[1],1);
    close(pfds1[0]);
    close(pfds2[0]);
    close(pfds2[1]);
    close(pfds1[1]);
    if(execlp("ls", "ls", NULL)==-1)
        {
            perror("Error in exec line 24\n");
            exit(1);
        }
}

else{
    if(!fork())
    {
        //printf("grep ppid is:%d\n", getppid());
        dup2(pfds1[0],0);
        dup2(pfds2[1],1);
        close(pfds1[0]);
        close(pfds2[0]);
        close(pfds2[1]);
        close(pfds1[1]);
        if(execlp("grep","grep","pipes",NULL)==-1)
            {
                perror("Error in exec line 41\n");
                exit(1);
                }
}   

    else{
    if(!fork())
    {   //printf("wc ppid is:%d\n", getppid());
        dup2(pfds2[0],0);
        close(pfds1[0]);
        close(pfds2[0]);
        close(pfds2[1]);
        close(pfds1[1]);
        if(execlp("wc","wc","-l",NULL)==-1)
            {
                perror("Error in exec line 56\n");
                exit(1);
            }
    }
    else{
        close(pfds1[0]);
        close(pfds2[0]);
        close(pfds1[1]);
        close(pfds1[1]);
        wait(&s);
        wait(&s);
        wait(&s);
        //printf("parent pid is:%d\n", getpid());
        //printf("grandparent pid is:%d\n", getppid());
        exit(0);
        
      }
  }
        
}
}

当只使用 2 个 wait(&s) 而不是 3 个时,即使有三个子进程,代码也能正常工作。当前代码卡住,没有完成执行。有人可以详细说明为什么会这样吗?

谢谢

首先,您的代码中有一个拼写错误,导致最后一个进程中管道的一端保持打开状态,从而导致阻塞发生。见下文。

问题是,当你 fork 你的两个文件描述符时(在调用 exec 之前,你正在关闭一个未使用的文件描述符,在 child 中)转换成四个(两个在 child、还有 parent 中的两个,并且您 不要关闭 parent 进程中的其中一个文件描述符)

如果您不关闭不使用的描述符(在 parent 或 child 进程中),在某些情况下,管道的两个描述符仍将打开,当发生这种情况时,读取过程被阻塞,等待一些输入(或 EOF)的到来。当你创建一个管道时(通过使用 pipe(2) 系统调用,一旦你 fork(),关闭你不打算使用的管道的文件描述符,因为你可以阻塞,因为一切都完成了,但您仍然等待输入(是 parent 或 child 过程,具体取决于您如何组织信息流动)

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

int main(void)
{
    //pid_t p;
    int pfds1[2],pfds2[2], s;
    pipe(pfds1);
    pipe(pfds2);

    if(!fork()) { //first child ls
        //printf("ls ppid is:%d\n", getppid());
        dup2(pfds1[1],1);

在这里,您已经将管道的写入端安装为 ls 的标准输出,但您没有阅读它。

        close(pfds1[0]);
        close(pfds2[0]);
        close(pfds2[1]);
        close(pfds1[1]);

没关系。所有文件描述符都将在退出时关闭,因此无需显式关闭它们。

        if(execlp("ls", "ls", NULL)==-1) {
            perror("Error in exec line 24\n");
            exit(1);
        }
    } else { // parent
        if(!fork()) {
            //printf("grep ppid is:%d\n", getppid());
            dup2(pfds1[0],0);
            dup2(pfds2[1],1);

此处将标准输入连接到 pipe1 的读取端,将 pipe2 的标准输出连接到标准输入。 grep 将等待该管道上的输入。

            close(pfds1[0]);
            close(pfds2[0]);
            close(pfds2[1]);
            close(pfds1[1]);

如前所述,当 grep 完成时,所有管道都将关闭...因此无需在此处明确关闭它们。

            if(execlp("grep","grep","pipes",NULL)==-1) {
                perror("Error in exec line 41\n");
                exit(1);
            }
        } else { // parent
            if(!fork()) {
                //printf("wc ppid is:%d\n", getppid());
                dup2(pfds2[0],0);

您还将 pipe2 的输入边缘连接到标准输入,因此 wc -l 将等待您在 pipe2 上的输入。

                close(pfds1[0]);
                close(pfds2[0]);
                close(pfds2[1]);
                close(pfds1[1]);

一如既往,如果您不关闭也没问题,因为此过程将在完成时关闭所有这些。

                if(execlp("wc","wc","-l",NULL)==-1) {
                    perror("Error in exec line 56\n");
                    exit(1);
                }
            } else{ // parent
                close(pfds1[0]);
                close(pfds2[0]);
                close(pfds1[1]);
                close(pfds1[1]);  /* mistake!!!! */

你上面有一个错字,你关闭了两次描述符 pdfs1[1] 并且 pdfs2[1] 保持打开状态,所以最后一个进程仍在等待管道上的更多输入,并且永远不会结束。

                wait(&s);
                wait(&s);
                wait(&s);
                //printf("parent pid is:%d\n", getpid());
                //printf("grandparent pid is:%d\n", getppid());
                exit(0);

            }
        }
    }
}

编辑

管道要求关闭属于写入端的所有描述符,以便通知 readers 文件结束已经发生,因此解锁进程 read()ing在他们。在 fork 中,管道的描述符被隐式 dup()ed,当您通过显式循环重定向输入或输出时,您再次 dup() 它们。 children 进程只需要关闭它们不打算使用的描述符(你做得很好,当你执行 dup() 重定向时,然后关闭所有管道描述符)

在你的情况下,你使用管道只是为了连接 children,并且没有从 parent...向 reader 发出信号。要从使用 EOF 的读取中解锁 reader,您需要关闭所有复制的写入描述符。这包括parent和其他children.