【问题标题】:read continues to block despite pipe closure尽管管道关闭,读取仍继续阻塞
【发布时间】:2016-03-22 20:38:08
【问题描述】:

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

  • 首先创建一个指向2 管道的5 int* 数组。父母使用第一个与孩子交谈,孩子使用第二个
  • 5 children 并关闭管道的适当末端
  • 每个孩子都在不断地尝试阅读
  • 父级关闭管道的所有写入端,因此子级中的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; 目标??!?

    【问题讨论】:

    • 每个智利都有一个duped fd...所以关闭“写入端”不会在子进程fd中关闭它,直到所有子进程也关闭写入端...你应该在 fork 时关闭子进程中的写入端,将引用计数降回 1。
    • @Myst,OP 确实关闭了这些:close(pipes[i][1]);
    • @SergeyA - 其中只有一个是关闭的。 OP 应该为每个 i 关闭
    • @Myst,哦,是的,是的,是的!没看到这个,被这个迷惑了。您应该将此作为答案发布。
    • 当您可以声明int pipes[PROCESSES][2]; 时,动态地为您的FD 分配空间有点……奇怪。然后你可以像你已经做的那样使用pipes[i],除了不malloc()他们(也不会因为没有free()他们而感到难过)。

    标签: c linux unix pipe posix


    【解决方案1】:

    首先,我将回顾一些您显然知道的信息。我写这个是因为其他人也可能会读到这个答案,而且为答案提供一些背景总是好的。

    之后我将在以下位置展示您的代码的原因:

    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) 的 duped 副本,每个 pipe 由两个文件描述符组成,一个用于输入(读取),另一个用于输出(写入)。

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

    read 将在传入数据最终到达的机会仍然存在时阻塞 - 意思是,read 将在至少一个“输出”(写入)文件描述符仍处于打开状态时阻塞。

    在下面的示例中,我将打开一个管道并 fork 进程。分叉的进程将关闭它的“输入”(写入)端点并调用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]);
        }
    
        //...
    

    附言

    为每个进程打开管道不太常见,除非每个进程都有单独的角色。

    共享同一功能的所有进程共享同一管道更为常见。

    例如,如果一个进程族用于执行共享的任务族,那么哪个进程执行哪个任务可能并不重要 - 因此,如果将任务提交到共享管道并且第一个读取数据的进程是执行任务的进程。

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

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

    【讨论】:

    • "父级关闭它的"输入" fd后,没有更多的写入端点并且读取将失败(停止阻塞)。",但是所有的文件描述符都是重复的,这大概包括STDIN_FILENO这是默认的1,但是我们没有在child中关闭它,为什么不阻塞read
    • @demonware - 我所指的输入端点是写入read 正在读取的同一管道的端点。 stdin 没有写入read 中使用的管道,因此无关紧要。把它想象成生物学中的物种。如果没有特定物种的雌性,该物种将灭绝(假设繁殖需要雌性)......拥有其他物种的雌性不是很有帮助;-)
    • 对不起,我的意思是STDOUT。还有为什么pipe[1][1] 可能会影响pipe[2][0] 的读取,这不是两个不同的管道吗?
    • @demonware - 你是对的。 stdoutpipe[1][1] 都对 pipe[2][0] 没有影响(你真的不应该将变量命名为 pipe,因为它会影响 pipe 函数)......但是 - pipe[2][1] 保持打开状态其他进程,这意味着read 仍在阻塞。
    • 哦,等等,我明白你的意思,对不起,是的,完全正确
    【解决方案2】:

    一些事情。

    (1) 对于每个孩子,您只创建了一个管道 [父到子],但您需要第二个 [子到父](即管道不是双向的像套接字)。

    (2) 当您预先创建 all 管道时,在子节点中,您必须关闭当前子节点的 not 管道,而不仅仅是它的两个管道的管道侧。

    如果您这样做,那么子 N 将为 每个 子级的两个管道,用于 所有 个子级 N.

    [给定] 分叉后,如果父级完全关闭打开的管道,子级将仍然继承 [a copy] 中打开的任何文件描述符分叉时的父级。因此,关闭父级没有任何效果,因为 child 仍然让它们保持打开状态——对于 所有 的子级

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

    在我的版本 [以下] 中,它不那么严重。如果没有预关闭(通过childclose),子 0 仅保持其自己的管道打开。但是,子 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;
    }
    

    【讨论】:

    • 只是好奇,但是为什么这会导致每个孩子中的read 被阻止? read 在特定管道上(不是全部),实际上我最终(从父级)关闭了所有管道
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-01-04
    • 1970-01-01
    • 2016-08-09
    • 1970-01-01
    • 2017-05-08
    • 2013-04-07
    相关资源
    最近更新 更多