尽管管道关闭,读取继续阻止

时间:2016-03-22 20:38:09

标签: c linux unix pipe posix

首先,要准备好看一些魔法 嗨,过去几个小时我一直对这个问题感到沮丧和挣扎,我无法理解为什么孩子的过程不会消亡。我基本上只有一个父进程和许多子进程。所有孩子都需要与父母沟通,家长需要与所有孩子沟通。对于初学者,我只是让孩子们不断尝试read,但我的父母什么也没有发送,只是关闭了管道的write端,从而导致他们的reads停止阻止。这是我的过程(我的宏def是5子进程):

  • 首先创建一个指向5 int*管道的2数组。父母使用第一个与孩子交谈,孩子使用第二个
  • 5个孩子并关闭管道的适当末端
  • 每个孩子不断尝试阅读
  • Parent关闭管道的所有写入结束,因此子节点中的read循环应终止
  • 父母等待儿童死亡
  • 父母去世
  • 这是我的代码:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #define PROCESSES 5
    
    
    int main(int argc, char ** argv) {
    
        int * pipes[PROCESSES];
        for (int i = 0; i < PROCESSES; i++) {
            pipes[i] = malloc(sizeof(int) * 2);
            if (pipe(pipes[i]) == -1) {
                perror("Error piping");
                exit(1);
            }
        }
    
        //PIDS we will wait on
        int children_pids[PROCESSES];
    
        for (int i = 0; i < PROCESSES; i++) {
            int status = fork();
            switch(status) {
                case -1:
                    perror("Error forking a child");
                    exit(1);
                case 0:
                    //Close the pipes we don't need
                    close(pipes[i][1]);
                    //Inside the child process, die immediately
                    char buffer[128] = "";
                    while (read(pipes[i][0], buffer, 127) > 0) {
                        //Keep reading and doing nothing
                    }
                    printf("Dying\n");
                    exit(1);
                default:
                    //Parent process, close the pipes we don't need
                    close(pipes[i][0]);
                    break;
            }
            //Parent continue spawning children
            children_pids[i] = status;
        }
    
        //CLOSE ALL PIPES FROM PARENT TO CHILDREN------------
        for (int i = 0; i < PROCESSES; i++) {
            if (close(pipes[i][1]) == -1) {
                perror("Error closing a pipe");
                exit(1);
            }
        }
    
        //AWAIT CHILDREN DEATHS
        for (int i = 0; i < PROCESSES; i++) {
            wait(&children_pids[i]);
        }
        printf("All children have died");
        return 0;
    }
    

    知道这是儿童中的read循环阻止儿童死亡,因为当它被移除时,它可以正常工作。但是,我无法弄清楚为什么会这样。在我的底部循环中,我清楚地关闭所有管道甚至检查错误。为什么是这样?! read如何阻止我完成return;目标?!

    2 个答案:

    答案 0 :(得分:4)

    一些事情。

    (1)对于每个孩子,你只创建了一个管道[parent-to-child],但你需要第二个[child-to-parent](即管道双向像插座一样。)

    (2)当您预先创建所有管道时,您必须关闭当前孩子的管道,而不仅仅是两个管道的管道侧面。

    如果执行此操作,则子N将为每个打开 [和]边 N

    一个[给定]分支之后,如果父代完全关闭了一个打开的管道,那么孩子仍然继承[副本]任何文件描述符在叉子时的父母。因此,在父级中关闭没有任何效果,因为孩子仍然将它们打开 - 对于孩子的所有

    这就是你原来的程序所做的。

    在我的[下面]版本中,它不那么严重。如果没有预关闭(通过childclose),子0只保持自己的管道打开。但是,Child 1将保持开放式0的管道。孩子2将为孩子0和孩子1打开管道。等等......

    因此,许多孩子正在互相抱着管道描述符打开。因此,当父进程关闭管道时,它们仍被其他子进行打开,因此 no 子进程将看到EOF

    如果您想要想象这一点,请在fork之后(例如,case 0之后),将原始代码作为孩子的第一个可执行部分:

    {
        pid_t pid = getpid();
        char buf[100];
        printf("DEBUG: %d\n",pid);
        sprintf(buf,"ls -l /proc/%d/fd",pid);
        system(buf);
    }
    

    忽略stdin / stdout / stderr而不是预期的2(应该是4个)打开描述符,你会在每个子项中看到(2 * PROCESSES)(即10个)描述符。

    在父母的最后关闭之后你可以重复[在父母中]这样的序列,你仍然看到同样的事情[减去每个孩子将关闭的两个]。

    使用结构可以更轻松地组织它。为了证明它确实有效,我添加了一些回显的实际数据传输。我还添加了一些调试选项来显示差异。

    下面是更正后的代码[请原谅无偿风格的清理]:

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/select.h>
    #include <sys/wait.h>
    
    #define PROCESSES 5
    
    int opt_n;                              // do _not_ close other children
    int opt_p;                              // original semantics
    int opt_v;                              // show list
    
    // child control
    struct child {
        int cld_idx;                        // child index
        pid_t cld_pid;                      // child's pid
        int cld_status;                     // child's exit status
        int cld_topar[2];                   // pipe: child-to-parent
        int cld_tocld[2];                   // pipe: parent-to-child
    };
    
    #define CLOSEME(_fd) \
        do { \
            if (_fd >= 0) \
                close(_fd); \
            _fd = -1; \
        } while (0)
    
    struct child children[PROCESSES];
    
    // fdlist -- output list of open descriptors
    void
    fdlist(struct child *cld,const char *reason)
    {
        struct child cld2;
        char cmd[100];
    
        if (cld == NULL) {
            cld = &cld2;
            cld->cld_pid = getpid();
            cld->cld_idx = -1;
        }
    
        printf("\n");
        printf("fdlist: idx=%d pid=%d (from %s)\n",
            cld->cld_idx,cld->cld_pid,reason);
    
        sprintf(cmd,"ls -l /proc/%d/fd",cld->cld_pid);
        system(cmd);
    }
    
    // childclose -- close any pipe units from other children
    void
    childclose(int i)
    {
        struct child *cld;
    
        for (int j = 0;  j < PROCESSES;  ++j) {
            if (j == i)
                continue;
            cld = &children[j];
            CLOSEME(cld->cld_topar[0]);
            CLOSEME(cld->cld_topar[1]);
            CLOSEME(cld->cld_tocld[0]);
            CLOSEME(cld->cld_tocld[1]);
        }
    }
    
    // childopen -- create pipes for child
    void
    childopen(int i)
    {
        struct child *cld;
    
        cld = &children[i];
    
        // to cut down on the clutter, only create the pipes as we need them
        pipe(cld->cld_topar);
        pipe(cld->cld_tocld);
    }
    
    // childstart -- start up child
    void
    childstart(int i)
    {
        struct child *cld;
        pid_t pid;
    
        cld = &children[i];
    
        // to cut down on the clutter, only create the pipes as we need them
        if (! opt_p)
            childopen(i);
    
        pid = fork();
        if (pid < 0) {
            perror("Error forking a child");
            exit(1);
        }
    
        switch (pid) {
        case 0:  // child
            // close any pipe that doesn't belong to us
            if (! opt_n)
                childclose(i);
    
            pid = getpid();
            cld->cld_pid = pid;
    
            if (opt_v)
                fdlist(cld,"childstart");
    
            // Close the pipe sides we don't need
            CLOSEME(cld->cld_topar[0]);
            CLOSEME(cld->cld_tocld[1]);
    
            // Inside the child process, die immediately
            int len;
            char buffer[128];
    
            while (1) {
                len = read(cld->cld_tocld[0], buffer, sizeof(buffer) - 1);
                if (len <= 0)
                    break;
    
                // Keep reading and echoing
                write(cld->cld_topar[1],buffer,len);
            }
    
            printf("child %d: Dying\n",i);
            exit(1);
            break;
    
        default:  // parent
            // give child time to print message
            if (opt_v)
                sleep(1);
    
            cld->cld_pid = pid;
    
            // Parent process, close the pipe sides we don't need
            CLOSEME(cld->cld_topar[1]);
            CLOSEME(cld->cld_tocld[0]);
    
            break;
        }
    }
    
    int
    main(int argc, char **argv)
    {
        char *cp;
        struct child *cld;
        int len;
        char buf[128];
    
        --argc;
        ++argv;
    
        for (;  argc > 0;  --argc, ++argv) {
            cp = *argv;
            if (*cp != '-')
                break;
    
            switch (cp[1]) {
            case 'n':  // do _not_ close other descriptors
                opt_n = 1;
                break;
            case 'p':  // preopen all pipes
                opt_p = 1;
                break;
            case 'v':  // show verbose messages
                opt_v = 1;
                break;
            }
        }
    
        setlinebuf(stdout);
    
        printf("main: pipes will be created %s\n",
            opt_p ? "all at once" : "as needed");
        printf("main: other child descriptors %s be closed\n",
            opt_n ? "will not" : "will");
    
        for (int i = 0; i < PROCESSES; i++) {
            cld = &children[i];
            cld->cld_idx = i;
            cld->cld_topar[0] = -1;
            cld->cld_topar[1] = -1;
            cld->cld_tocld[0] = -1;
            cld->cld_tocld[1] = -1;
        }
    
        // create pipes for _all_ children ahead of time
        if (opt_p) {
            for (int i = 0; i < PROCESSES; i++)
                childopen(i);
            if (opt_v)
                fdlist(NULL,"master/OPEN");
        }
    
        // start up all children
        for (int i = 0; i < PROCESSES; i++)
            childstart(i);
    
        // show final list
        if (opt_v) {
            sleep(1);
            for (int i = 0; i < PROCESSES; i++) {
                cld = &children[i];
                fdlist(cld,"master/POSTSTART");
            }
        }
    
        // send to child
        for (int i = 0; i < PROCESSES; i++) {
            cld = &children[i];
            len = sprintf(buf,"child %d, you are pid %d\n",i,cld->cld_pid);
            write(cld->cld_tocld[1],buf,len);
        }
    
        // receive from child
        printf("\n");
        for (int i = 0; i < PROCESSES; i++) {
            cld = &children[i];
            len = read(cld->cld_topar[0],buf,sizeof(buf));
            printf("RECV(%d): %s",i,buf);
        }
    
        // show final list
        if (opt_v) {
            sleep(1);
            for (int i = 0; i < PROCESSES; i++) {
                cld = &children[i];
                fdlist(cld,"master/FINAL");
            }
        }
    
        // CLOSE ALL PIPES FROM PARENT TO CHILDREN------------
        for (int i = 0; i < PROCESSES; i++) {
            cld = &children[i];
            CLOSEME(cld->cld_topar[0]);
            CLOSEME(cld->cld_tocld[1]);
        }
    
        // AWAIT CHILDREN DEATHS
        for (int i = 0; i < PROCESSES; i++) {
            cld = &children[i];
            waitpid(cld->cld_pid,&cld->cld_status,0);
        }
    
        printf("All children have died\n");
    
        return 0;
    }
    

    答案 1 :(得分:4)

    首先,我会回顾一些您明显知道的信息。我写这篇文章是因为其他人也可能会读到这个答案,因为总有一些答案是有好处的。

    之后我将在以下位置显示您的代码:

    case 0: // the child process
        close(pipes[i][1]); // <- if (i == 2) pipes[1][1] is open. 
    

    可能意味着执行了以下任务:

    case 0: // the child process
        // close all input endpoints (input only performed by root process)
        // also close all irrelevant output endpoints:
        for (int j = 0; j < PROCESSES; j++){
          close(pipes[j][1]);
          if(j != i)
            close(pipes[j][0]);
        }
    

    众所周知,每个子进程都会收到文件描述符(fd)的dup副本,每个pipe由两个文件描述符组成,一个用于输入(读取),另一个用于输入(读取)输出(写作)。

    每次分叉流程时,这两个端点(文件描述符) - 用于每个打开管道 - 都是重复的。

    read将阻止,但传入的数据仍有可能最终到达 - 这意味着read将阻塞,而至少一个“输出”(写入)文件描述符仍然打开。 / p>

    在下面的示例中,我将打开一个管道并分叉该过程。分叉进程将关闭它的“输入”(写入)端点并调用readread将阻止,因为父进程中仍有一个打开的输入fd(请记住,fd是重复的)。在父关闭它的“输入”fd之后,没有更多的写入端点,读取将失败(停止阻塞)。

    注意,我没有在管道上写任何东西。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    typedef struct {
      int in;   // the input fd
      int out;  // the output fd
    } pipe_io;
    
    int main() {
      // the container;
      pipe_io io;
      // make the pipe
      pipe((int*)&io);
      // forking will duplicate the open files
      pid_t child;
      if (!(child = fork())) {  // fork==0 => we're in the child process
        close(io.out);          // closing one reading access point.
        char buff[4];
        // read will block because there's still an open writing access point.
        printf("Child waiting (read will block)\n");
        read(io.in, buff, 1);
        // cleanup and exit process.
        close(io.in);
        printf("Child exits (read stopped blocking once all inputs were closed)\n");
        exit(0);
      }
      sleep(1);  // wait...
      printf("closing parent's writing (output) endpoint.\n");
      close(io.out);
      sleep(1);  // wait...
      printf("closing parent's reading (input) endpoint.\ndone.\n");
      waitpid(child, NULL, 0);
    }
    

    输出是代码控制流程的明确指示:

    Child waiting (read will block)
    closing parent's writing (output) endpoint.
    Child exits (read stopped blocking once all inputs were closed)
    closing parent's reading (input) endpoint.
    done.
    

    因此,为了调用read失败(不阻塞),我们需要关闭所有写入端点/通道。

    在您的代码中,每个进程都有一个管道,但是您允许每个进程保持其他进程的“输入”(写入)端点打开 - 因此read将始终阻塞。

    case 0: // the child process
        // This line only closes this process's input stream, but this stream is
        // open for all other processes:
        close(pipes[i][1]); // <- if (i == 2) pipes[1][1] is open. 
    
        //...
    
        // `read` will ALWAYS block because other processes keep input endpoints.
        while (read(pipes[i][0], buffer, 127) > 0) {
            //Keep reading and doing nothing
        }
        printf("Dying\n");
        exit(1);
    

    你可能想写:

    case 0: // the child process
        // closing all input endpoints (input only performed by root process)
        // also closing all irrelevant output endpoints:
        for (int j = 0; j < PROCESSES; j++){
          close(pipes[j][1]);
          if(j != i)
            close(pipes[j][0]);
        }
    
        //...
    

    P.S。

    除非每个流程都有一个单独的角色,否则为每个流程打开一个管道是不常见的。

    共享相同功能的所有进程通常共享同一个管道。

    例如,如果使用一系列进程来执行共享的任务系列,则哪个进程执行哪个任务可能并不重要 - 因此如果将任务提交到共享管道并且第一个任务被提交,则更有效读取数据的过程是执行任务的过程。

    当进程忙于执行任务时,它不会从管道读取,如果有另一个进程可用(阻塞“读取”),它会立即投入使用(而不是等待繁忙的进程)。

    这种单一管道设计最大限度地减少了“等待”时间段,并消除了任何调度问题(管道缓冲区的限制除外)。