尽管管道的写入端已关闭,为什么 read() 会在 parent 进程中永远阻塞并等待?

Why does read() block and wait forever in parent process despite the writing end of pipe being closed?

我正在编写一个程序,其中包含两个通过管道进行通信的进程。 child 进程从 parent 读取一些参数,用它们执行 shell 脚本,然后 return 将结果逐行发送到 parent 进程。

我的代码工作得很好,直到我在 parent 过程结束时编写了 while(read()) 部分。 child 将执行 shell 脚本,从 popen() 读取其回显并将它们打印到标准输出。

现在我也尝试将结果写入管道,并在 parent 末尾的 while() 循环中读取它们,但它会阻塞,child 也不会进程将结果打印到标准输出。 Apparently 在从 parent.

发送的管道中读取数据后,它甚至不会到达该点

如果我在parent过程中注释掉while(),child会打印结果,return,程序就顺利结束了。

为什么即使我在 parent 和 child 进程中都关闭了管道的写入端,while(read()) 也会阻塞?

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

int read_from_file(char **directory, int *octal) {
        FILE *file = fopen("input", "r");
        if (file == NULL) {
                perror("error opening file");
                exit(1);
        }
        fscanf(file, "%s %d", *directory, octal);
}

int main(int argc, char *argv[]) {

        char *directory = malloc(256);
        int *octal = malloc(sizeof *octal);

        pid_t pid;
        int pfd[2];
        char res[256];
        if (pipe(pfd) < 0) {
                perror("Error opening pipe");
                return 1;
        }

        if ((pid = fork()) < 0)
                perror("Error forking");

        if (pid == 0) {
                printf("client here\n");
                if (read(pfd[0], directory, 256) < 0)
                        perror("error reading from pipe");
                if (read(pfd[0], octal, sizeof(int)) < 0)
                        perror("error reading from pipe");
// This won't get printed:
                printf("client just read from pipe\n");
//              close(pfd[0]);

                char command[256] = "./asd.sh ";
                strcat(command, directory);
                char octal_c[5];
                sprintf(octal_c, " %d", *octal);
                strcat(command, octal_c);

                FILE *f = popen(command, "r");
                while (fgets(res, 256, f) != NULL) {
                        printf("%s", res);
                        if (write(pfd[1], res, 256) < 0)
                                perror("Error writing res to pipe");
                }
                fclose(f);
                close(pfd[1]);
                close(pfd[0]);
                fflush(stdout);
                return 1;
        }

        read_from_file(&directory, octal);

        if (write(pfd[1], directory, 256) < 0)
                perror("Error writing dir to pipe");
        if (write(pfd[1], octal, sizeof(int)) < 0)
                perror("error writing octal to pipe");

        int r;
        close(pfd[1]);

        while (r = read(pfd[0], res, 256)) {
                if (r > 0) {
                        printf("%s", res);
                }
        }
        close(pfd[0]);

        while (wait(NULL) != -1 || errno != ECHILD);
}

父进程试图在子进程读取管道并将结果写入管道之前读取管道。使用两个不同的管道进行双向通信解决了这个问题。

由于 child 明显达到...

                printf("client here\n");

...但似乎达不到...

                printf("client just read from pipe\n");

... 我们可以假设它无限期地阻塞在两个 read() 调用之一。在正确的时机,这解释了为什么 parent 自己 read() 从管道中阻塞。但是这种阻塞是如何以及为什么会发生的?

您的程序中至少存在三个严重的语义错误:

  1. 管道不适用于双向通信。例如,一个进程有可能读回它自己写入并打算用于不同进程的字节。如果您想要双向通信,请使用两个管道。在你的情况下,我认为这可以避免 apparent 死锁,尽管它本身不会使程序正常工作。

  2. writeread 不一定传输请求的全部字节数,短读写不认为是错误的。成功时,这些函数 return 传输的字节数,如果你想确保传输特定数量的字节那么你需要 运行 readwrite 在一个循环中,使用 return 值来跟踪正在传输的缓冲区的进度。或者改用 fread()fwrite()

  3. 管道传送无差别的字节流。也就是说,它们不是面向消息的。假设从管道读取将与对管道的写入配对是不安全的,这样每次读取都准确地接收到一次写入写入的字节。然而您的代码取决于它的发生。

这里有一个似是而非的失败场景,可以解释您的观察结果:

parent:

  1. fork()s child.
  2. 一段时间后对管道执行两次写入,一次来自变量 directory,另一次来自变量 octal。至少其中的第一个是简短的写作。
  3. 关闭管道写入端的副本。
  4. 阻止尝试从管道读取。

child:

  1. 读取所有通过其第一次读取写入的字节(进入其directory的副本)。
  2. 第二个 read() 阻塞。尽管 parent 关闭了其写入端的副本,它仍可以执行此操作,因为管道的写入端在 child.
  3. 中仍处于打开状态

然后你就陷入了僵局。管道的两端至少在一个进程中打开,管道为空,并且两个进程都被阻止试图读取永远无法到达的字节。

还有其他的可能性也基本相同,其中一些不依赖于短写。